"""You are an expert assistant specialized in the Ormophine MySQL Python ORM.
The text below this line is the COMPLETE source code of the Ormophine MySQL ORM library.
Your sole reference for answering any question is this code.
When a user asks about usage, errors, features, or implementation details, analyze the code and provide accurate, clear answers.
Include relevant code snippets and explain how they relate to the user's question.
Do not mention that you are an AI; simply respond as a knowledgeable human expert.
Be concise, helpful, and practical."""




from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal

<<<<<<< Updated upstream
=======


from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal

>>>>>>> Stashed changes


from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal

<<<<<<< Updated upstream
=======


from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from MySQLdb.converters import conversions as _mysql_conversions
from MySQLdb.constants import FIELD_TYPE
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from MySQLdb.converters import conversions as _mysql_conversions
from MySQLdb.constants import FIELD_TYPE
from queue import SimpleQueue, Empty
from typing import Literal



from . import TableStructure, Table
from MySQLdb import connect, OperationalError, ProgrammingError
from queue import SimpleQueue, Empty
from typing import Literal

>>>>>>> Stashed changes
class Driver():
    """
    MySQL connection manager with thread-safe connection pooling and ORM capabilities.

    The ``Driver`` class serves as the main entry point for database interactions.
    It manages a pool of MySQL connections (using ``SimpleQueue``) for thread-safe
    execution of queries, automatically handles reconnection on connection errors,
    and dynamically maps existing tables to :class:`Table` objects as attributes.

    When instantiated, it establishes an initial connection to validate credentials,
    optionally creates the database, sets up a connection pool of the specified size,
    and introspects the database to create :class:`Table` instances for every table,
    attaching them as attributes (e.g., ``db.users``, ``db.orders``). Each table
    attribute provides access to columns, CRUD operations, batch operations, joins,
    and schema management.

    The driver supports MySQL 8.0+ features, including transaction isolation levels,
    SQL modes, and InnoDB flush settings. It also provides user management methods
    (create, drop, grant, revoke) and administrative utilities.

    Attributes:
        host (str): Database server hostname or IP address.
        port (int): Database server port number.
        username (str): MySQL username for authentication.
        password (str): MySQL password.
        db_name (str): Name of the default database to connect to.
        charset (CHARSET): Character set for the connection (default ``'utf8mb4'``).
        collate (COLLATE): Collation for the connection (default ``'utf8mb4_bin'``).
        connect_timeout (int): Connection timeout in seconds (default ``10``).
        sql_modes (list): List of SQL modes to enable on each connection.
        config (dict): Full configuration dictionary passed to MySQLdb connections.
        connection_pool (SimpleQueue): Queue holding available (connection, cursor) tuples.
        connection_pool_storage (list): List of all connection objects for cleanup.
        _connected (bool): Internal flag indicating whether the driver is active.
        PLACE_HOLDER (str): String used as a placeholder in bulk operations for
            parameter substitution (default ``'_MY_S4ULT3D_PL4C3_H0LD3R_%s_'``).

    Note:
        All public database operations are thread-safe because each query acquires
        a dedicated connection from the pool and returns it after commit/rollback.

    Example:
        Create a driver instance and interact with the database::

            from ormophine.Mysql import Driver, DataTypes, TableStructure

            # Connect to an existing database
            db = Driver(
                host='localhost',
                port=3306,
                username='root',
                password='secret',
                db_name='myapp',
                pool_size=10
            )

            # Access a table dynamically
            users = db.users
            print(users.get_columns_name())

            # Perform a query
            results = users.get_row(
                which_columns=[users.id, users.name],
                where=users.age > 18
            )

            # Create a new table using TableStructure
            new_table = (TableStructure('products')
                         .add_column('id', DataTypes.INT(), primary_key=True,
                                     auto_increment=True, not_null=True)
                         .add_column('name', DataTypes.VARCHAR(100), not_null=True)
                         .add_column('price', DataTypes.DECIMAL(10,2)))
            db.create_table(new_table)

            # Use the newly created table
            db.products.insert({'name': 'Laptop', 'price': 999.99})

            # Disconnect when done
            db.disconnect()
    """
    CHARSET = Literal[
    "armscii8",
    "ascii",
    "big5",
    "binary",
    "cp1250",
    "cp1251",
    "cp1256",
    "cp1257",
    "cp850",
    "cp852",
    "cp866",
    "cp932",
    "dec8",
    "eucjpms",
    "euckr",
    "gb18030",
    "gb2312",
    "gbk",
    "geostd8",
    "greek",
    "hebrew",
    "hp8",
    "keybcs2",
    "koi8r",
    "koi8u",
    "latin1",
    "latin2",
    "latin5",
    "latin7",
    "macce",
    "macroman",
    "sjis",
    "swe7",
    "tis620",
    "ucs2",
    "ujis",
    "utf16",
    "utf16le",
    "utf32",
    "utf8mb3",
    "utf8mb4"
    ]
    COLLATE = Literal[
    "utf8mb4_0900_ai_ci",
    "utf8mb4_0900_as_cs",
    "utf8mb4_0900_bin",
    "utf8mb4_general_ci",
    "utf8mb4_unicode_ci",
    "utf8mb4_unicode_520_ci",
    "utf8mb4_bin",
    "utf8mb4_persian_ci",
    "utf8mb4_ar_0900_ai_ci",
    "utf8mb4_da_0900_ai_ci",
    "utf8mb4_de_pb_0900_ai_ci",
    "utf8mb4_en_0900_ai_ci",
    "utf8mb4_es_0900_ai_ci",
    "utf8mb4_es_trad_0900_ai_ci",
    "utf8mb4_fr_0900_ai_ci",
    "utf8mb4_it_0900_ai_ci",
    "utf8mb4_nl_0900_ai_ci",
    "utf8mb4_pt_0900_ai_ci",
    "utf8mb4_cs_0900_ai_ci",
    "utf8mb4_hr_0900_ai_ci",
    "utf8mb4_hu_0900_ai_ci",
    "utf8mb4_pl_0900_ai_ci",
    "utf8mb4_ro_0900_ai_ci",
    "utf8mb4_sk_0900_ai_ci",
    "utf8mb4_sl_0900_ai_ci",
    "utf8mb4_sv_0900_ai_ci",
    "utf8mb4_nb_0900_ai_ci",
    "utf8mb4_nn_0900_ai_ci",
    "utf8mb4_is_0900_ai_ci",
    "utf8mb4_lt_0900_ai_ci",
    "utf8mb4_lv_0900_ai_ci",
    "utf8mb4_et_0900_ai_ci",
    "utf8mb4_bg_0900_ai_ci",
    "utf8mb4_sr_latn_0900_ai_ci",
    "utf8mb4_bs_0900_ai_ci",
    "utf8mb4_mk_0900_ai_ci",
    "utf8mb4_ja_0900_as_cs",
    "utf8mb4_ko_0900_as_cs",
    "utf8mb4_zh_0900_as_cs",
    "utf8mb4_tr_0900_ai_ci",
    "utf8mb4_vi_0900_ai_ci",
    "utf8mb4_0900_as_cs",
    "utf8mb4_da_0900_as_cs",
    "utf8mb4_es_0900_as_cs",
    "utf8mb4_fr_0900_as_cs",
    "utf8mb4_it_0900_as_cs",
    "utf8mb4_ja_0900_as_cs",
    "utf8mb4_ko_0900_as_cs",
    "utf8mb4_zh_0900_as_cs",
    "utf8mb4_croatian_ci",
    "utf8mb4_czech_ci",
    "utf8mb4_danish_ci",
    "utf8mb4_esperanto_ci",
    "utf8mb4_estonian_ci",
    "utf8mb4_german2_ci",
    "utf8mb4_hungarian_ci",
    "utf8mb4_icelandic_ci",
    "utf8mb4_latvian_ci",
    "utf8mb4_lithuanian_ci",
    "utf8mb4_polish_ci",
    "utf8mb4_romanian_ci",
    "utf8mb4_slovak_ci",
    "utf8mb4_slovenian_ci",
    "utf8mb4_swedish_ci",
    "utf8mb4_turkish_ci"
    ]
    ISOLATION_LEVEL = Literal['READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE']
    INNODB_FLUSH_LOG = Literal[0,1,2]
    PRIVILEGES = Literal['ALL PRIVILEGES', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'CREATE', 'INDEX', 'REFERENCES', 'EXECUTE', 'GRANT OPTION', 'TRIGGER']
    def __init__(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        db_name: str,
        create_new_db: bool = False,
        pool_size: int = 5,
        connect_timeout: int = 10,
        charset: CHARSET = "utf8mb4",
        collate: COLLATE = "utf8mb4_bin",
        sql_modes: list = None,
        isolation_level: ISOLATION_LEVEL = 'REPEATABLE READ'
    ):
        """
        Initialize a new MySQL database driver with connection pooling and table ORM.

        This constructor establishes a connection pool to the specified MySQL database,
        optionally creates the database if it does not exist, and dynamically attaches
        `Table` objects for each existing table as attributes of the driver instance.
        The driver uses a thread‑safe queue to manage connections and supports custom
        SQL modes, transaction isolation levels, and InnoDB flush settings.

        Args:
            host (str): MySQL server hostname or IP address.
            port (int): TCP port number of the MySQL server.
            username (str): Username for authentication.
            password (str): Password for authentication.
            db_name (str): Name of the database to connect to (or create).
            create_new_db (bool, optional): If ``True``, create the database if it
                does not exist. Defaults to ``False``.
            pool_size (int, optional): Number of connections to keep in the pool.
                Defaults to 5.
            connect_timeout (int, optional): Connection timeout in seconds.
                Defaults to 10.
            charset (CHARSET, optional): Character set for the connection.
                Defaults to ``"utf8mb4"``. Must be a valid MySQL charset literal.
            collate (COLLATE, optional): Collation for the connection.
                Defaults to ``"utf8mb4_bin"``. Must be a valid MySQL collation literal.
            sql_modes (list, optional): List of additional SQL modes to enable
                (e.g., ``['ANSI_QUOTES']``). Defaults to an empty list.
                The session always enables ``PIPES_AS_CONCAT`` automatically.
            isolation_level (ISOLATION_LEVEL, optional): Transaction isolation level.
                Must be one of ``'READ UNCOMMITTED'``, ``'READ COMMITTED'``,
                ``'REPEATABLE READ'``, or ``'SERIALIZABLE'``.
                Defaults to ``'REPEATABLE READ'``.

        Raises:
            RuntimeError: If the connection pool cannot be created or the database
                does not exist and ``create_new_db`` is ``False``.
            OperationalError: If connection fails due to network issues, authentication
                errors, or other MySQL server problems.
            ProgrammingError: If the SQL syntax for creating the database is invalid.
            Exception: If the initial connection attempt fails and the pool cannot be
                replenished.

        Example:
            >>> from ormophine.Mysql import Driver
            >>> db = Driver(
            ...     host='localhost',
            ...     port=3306,
            ...     username='root',
            ...     password='secret',
            ...     db_name='my_app',
            ...     create_new_db=True,
            ...     pool_size=10,
            ...     charset='utf8mb4',
            ...     isolation_level='READ COMMITTED'
            ... )
            >>> # Existing tables are now available as attributes, e.g. db.users
            >>> users_table = db.users
            >>> # Use the driver to execute raw queries
            >>> db.custom_execute("SET SESSION wait_timeout = 28800")

        Notes:
            - The driver automatically adds `Table` attributes for every table
            currently in the database. For example, if a table named `orders`
            exists, it can be accessed as ``db.orders``.
            - The connection pool is implemented using a :class:`queue.SimpleQueue`
            and is thread‑safe. Each connection is wrapped with a cursor.
            - The constructor sets the session SQL mode to include ``PIPES_AS_CONCAT``
            to allow the ``||`` operator for string concatenation, which is used by
            the ORM's :class:`ColumnsOperation` and :class:`Column` classes.
            - When `create_new_db` is ``True``, the database is created with the
            given charset and collation. If the database already exists, no error
            is raised.
            - If a connection breaks during operation, the driver automatically
            recreates it and puts it back into the pool.
        """
        self.CONNECTION_ERRORS = (2002, 2003, 2005, 2006, 2012, 2013, 2026, 2049, 2055, 2000)
        self.host = host
        self.port = port
        self._connected = True
        self.username = username
        self.password = password
        self.db_name = db_name
        self.charset = charset
        self.collate = collate
        self.connect_timeout = connect_timeout
        self.sql_modes = [] if sql_modes is None else sql_modes
        self.config = {
            "host":self.host,
            "port":self.port,
            "user":self.username,
            "password":self.password,
            "db":self.db_name,
            "charset":self.charset,
            "connect_timeout":self.connect_timeout,
            "init_command": f'SET SESSION TRANSACTION ISOLATION LEVEL {isolation_level};'
        }
        self.connection_pool = SimpleQueue()
        self.connection_pool_storage = []
        
        #To make sure inputs are valid
        conf = {
        "host":self.host,
        "port":self.port,
        "user":self.username,
        "password":self.password,
        "charset":self.charset,
        "connect_timeout":self.connect_timeout
        }
        connection = connect(**conf)
        if not create_new_db:
            try:
                connection.select_db(self.db_name)
                connection.close()
            except Exception:
                connection.close()
                raise
        else:
            try:
                cur = connection.cursor()
                query = f"CREATE DATABASE {self.db_name} CHARACTER SET {self.charset} COLLATE {self.collate};"
                cur.execute(query)
                connection.close()
            except Exception:
                connection.close()
                print(query)
                raise                

        [self._create_connection() for _ in range(pool_size)]

        for i in self.get_tables():
            self.__setattr__(i, Table(self, i))

    def _create_connection(self):
        """
        Create a new database connection and add it to the connection pool.

        This method establishes a new MySQL connection using the stored configuration
        (host, port, username, password, database, charset, etc.) and initializes its
        cursor. The connection and cursor are then placed into the pool for later use.
        The session is configured to enable the `PIPES_AS_CONCAT` SQL mode and any
        additional modes provided during driver initialization.

        This method is called automatically when the pool is empty and a connection
        is requested via :meth:`_get_connection`. It ensures that the pool always has
        available connections.

        Raises:
            RuntimeError: If the driver has been disconnected (i.e., :attr:`_connected`
                is ``False``), creating new connections is not allowed.
            MySQLdb.OperationalError: If the connection attempt fails due to network
                issues, invalid credentials, or other operational errors.

        Example:
            # Internal usage within the driver:
            driver = Driver(host='localhost', username='root', password='pass', db_name='test')
            # If the pool is empty, _get_connection will call _create_connection automatically.
            con, cur = driver._get_connection()
            # ... use con/cur ...
            driver.connection_pool.put((con, cur))
        """
        if not self._connected:
            raise RuntimeError('You have closed the connection, you can not create new connections')
        try:
            con = connect(**self.config)
            cur = con.cursor()
            self.connection_pool.put((con, cur))
            self.connection_pool_storage.append(con)
            cur.execute("SET SESSION sql_mode = 'PIPES_AS_CONCAT';")
            for i in self.sql_modes:
                cur.execute(f"SET SESSION sql_mode = CONCAT(@@sql_mode, ',{i}');")
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:  
                con = connect(**self.config)
                cur = con.cursor()
                self.connection_pool.put((con, cur))
                self.connection_pool_storage.append(con)
                cur.execute("SET SESSION sql_mode = 'PIPES_AS_CONCAT';")
                for i in self.sql_modes:
                    cur.execute(f"SET SESSION sql_mode = CONCAT(@@sql_mode, ',{i}');")
            else:
                raise

    def _get_connection(self):
        """
        Retrieve a database connection and its cursor from the connection pool.

        This method attempts to get a (connection, cursor) pair from the internal
        :class:`~queue.SimpleQueue` pool. If the pool is empty, it creates a new
        connection (via :meth:`_create_connection`) and retries. If the pool is
        still empty after that, an exception is raised.

        This is an internal helper used by all execution methods
        (``_exc``, ``_excp``, ``_excf``, ``_excfp``, ``_excs``, ``_excm``) to
        obtain a working connection in a thread‑safe manner.

        Returns:
            tuple: A pair ``(connection, cursor)`` where both are active MySQL
            connection objects from the pool.

        Raises:
            Exception: If the connection pool remains empty after attempting to
                create a new connection. This typically indicates that the pool
                size is too small and the timeout (0.5 seconds) is insufficient,
                or that the database server is unreachable.

        Example:
            .. code-block:: python

                # Internal usage only
                con, cur = driver._get_connection()
                try:
                    cur.execute("SELECT 1")
                    result = cur.fetchone()
                    con.commit()
                finally:
                    driver.connection_pool.put((con, cur))
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        try:
            return self.connection_pool.get(block=True, timeout=0.5)
        except Empty:
            self._create_connection()
            try:
                return self.connection_pool.get(block=True, timeout=0.5)
            except Empty as e:
                raise Exception(f'{e}\n\nEmpty connection pool, you better increase `pool_size`')

    def _excfp(self, query, params):
        """Execute a parameterized SQL query and return all fetched rows.

        This method acquires a database connection from the internal connection pool,
        executes the given query with the provided parameters, commits the transaction,
        and returns the complete result set. It automatically handles connection
        failures (e.g., server has gone away) by recreating the connection and
        retrying the operation once. In case of any SQL error, the transaction is
        rolled back, the connection is returned to the pool, and an exception with
        detailed context (including the query and parameters) is raised.

        This is an internal method primarily used by public methods like
        :meth:`Driver.custom_execute_with_fetch` and :meth:`Table.get_table_info`.

        Args:
            query (str): The SQL query to execute. Use ``%s`` placeholders for
                parameters (MySQLdb style).
            params (list, tuple, or dict): The parameter values to bind to the
                query placeholders. The type must be compatible with the MySQLdb
                cursor's ``execute()`` method.

        Returns:
            list of tuple: All rows returned by the query. Each row is represented
            as a tuple of column values.

        Raises:
            Exception: Wraps any underlying :class:`MySQLdb.OperationalError` or
                :class:`MySQLdb.ProgrammingError`. The raised exception includes the
                original error message, the query string, and the parameter values
                to aid debugging.

        Example:
            Assuming a ``Driver`` instance ``db`` and a table ``users`` with columns
            ``id`` and ``name``:

            >>> result = db._excfp("SELECT id, name FROM users WHERE age > %s", (18,))
            >>> print(result)
            [(1, 'Alice'), (3, 'Charlie')]

        Note:
            This method uses the connection pool. If the pool is empty, it will
            create a new connection (up to the pool size limit). It is safe for
            concurrent use.
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            cur.execute(query, params)
            res = cur.fetchall()
            con.commit()
            self.connection_pool.put((con, cur))
            return res
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    cur.execute(query, params)
                    res = cur.fetchall()
                    con.commit()
                    self.connection_pool.put((con, cur))
                    return res
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')

    def _excf(self, query):
        """
        Execute a read-only SQL query and fetch all results.

        This internal method retrieves a connection from the pool, executes the
        given query without parameters, fetches all rows, and returns them.
        It handles connection errors by automatically reconnecting and retrying
        once. The method also manages transaction commits and rollbacks, and
        returns the connection to the pool after execution.

        **Note:** This method is intended for internal use by other methods in
        the :class:`Driver` class. It is used when no parameter substitution is
        required and the query is expected to return result sets (e.g., SELECT).

        Args:
            query (str): The SQL query string to execute. Must not contain
                parameter placeholders (use :meth:`_excfp` for parameterized
                queries).

        Returns:
            tuple: A tuple of rows returned by the query. Each row is a tuple
                of column values as returned by the MySQL driver.

        Raises:
            Exception: If an :class:`MySQLdb.OperationalError` occurs that is not
                a connection error, or if a :class:`MySQLdb.ProgrammingError` is
                raised. In these cases, the original error message is augmented
                with the query text to aid debugging. Connection errors are
                handled internally and a retry is attempted.

        Example:
            .. code-block:: python

                driver = Driver(...)
                # Fetch all table names in the current database
                rows = driver._excf("SHOW TABLES")
                for (table_name,) in rows:
                    print(table_name)

        .. seealso:: :meth:`_excfp` for parameterized queries that return results.
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            cur.execute(query)
            res = cur.fetchall()
            con.commit()
            self.connection_pool.put((con, cur))
            return res
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    cur.execute(query)
                    res = cur.fetchall()
                    con.commit()
                    self.connection_pool.put((con, cur))
                    return res
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                raise Exception(f'{e}\nQuery:\n\t{query}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}')
    
    def _excp(self, query, params):
        """
        Execute a parameterized SQL query and commit the transaction.

        This internal method retrieves a connection from the pool, executes the provided
        query with the given parameters, commits the transaction, and returns the
        connection to the pool. If a connection error occurs (e.g., server gone away),
        it attempts to reconnect and retry the execution. On any other SQL error,
        it rolls back the transaction and raises an exception with detailed context.

        Args:
            query (str): The SQL query string containing placeholders (``%s``) for
                parameters.
            params (list or tuple): The parameter values to substitute into the query.

        Returns:
            None: This method does not return any value; it only executes the query.

        Raises:
            Exception: If an operational or programming error occurs. The exception
                message includes the original error, the query, and the parameters
                to aid debugging.
            RuntimeError: If the connection pool is empty and a new connection cannot
                be created (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance ``db`` and a table ``users``::

                db._excp(
                    "INSERT INTO users (name, age) VALUES (%s, %s)",
                    ("Alice", 30)
                )
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            cur.execute(query, params)
            con.commit()
            self.connection_pool.put((con, cur))
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    cur.execute(query, params)
                    con.commit()
                    self.connection_pool.put((con, cur))
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
    
    def _exc(self, query):
        """
        Execute a SQL query without parameters and commit the transaction.

        This internal method retrieves a connection from the pool, executes the provided
        query (which should contain no placeholders), commits the transaction, and
        returns the connection to the pool. If a connection error occurs (e.g., server
        gone away), it attempts to reconnect and retry the execution. On any other SQL
        error, it rolls back the transaction and raises an exception with detailed
        context.

        Args:
            query (str): The SQL query string to execute (no parameters).

        Returns:
            None: This method does not return any value; it only executes the query.

        Raises:
            Exception: If an operational or programming error occurs. The exception
                message includes the original error and the query to aid debugging.
            RuntimeError: If the connection pool is empty and a new connection cannot
                be created (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance ``db``::

                # Create a table
                db._exc("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50));")

                # Drop a table (use with caution)
                db._exc("DROP TABLE IF EXISTS temp;")
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            cur.execute(query)
            con.commit()
            self.connection_pool.put((con, cur))
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    cur.execute(query)
                    con.commit()
                    self.connection_pool.put((con, cur))
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                raise Exception(f'{e}\nQuery:\n\t{query}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}')
    
    def _excs(self, query_params: list):
        """
        Execute a script of multiple parameterized SQL queries in a single transaction.

        This internal method processes a list of query definitions, each of which may
        include parameters. It retrieves a connection from the pool, executes each
        query in sequence, commits the transaction, and returns the connection to
        the pool. If a connection error occurs (e.g., server gone away), it attempts
        to reconnect and retry the entire script. On any other SQL error, it rolls
        back the transaction and raises an exception with detailed context.

        Args:
            query_params (list): A list of query definitions. Each item can be:
                - A list of length 2: ``[query_string, params_list]`` for a
                parameterized query.
                - A list of length 1: ``[query_string]`` for a query without
                parameters.

        Returns:
            None: This method does not return any value; it only executes the queries.

        Raises:
            Exception: If an operational or programming error occurs. The exception
                message includes the original error and a formatted list of all
                queries and their parameters to aid debugging.
            RuntimeError: If the connection pool is empty and a new connection cannot
                be created (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance ``db`` and a table ``users``::

                script = [
                    ["INSERT INTO users (name, age) VALUES (%s, %s)", ("Alice", 30)],
                    ["UPDATE users SET age = %s WHERE name = %s", (31, "Alice")],
                    ["DELETE FROM users WHERE age < %s", (18,)]
                ]
                db._excs(script)
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            for q in query_params:
                if len(q) == 2:
                    cur.execute(q[0], q[1])
                else:
                    cur.execute(q[0])
            con.commit()
            self.connection_pool.put((con, cur))
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    for q in query_params:
                        if len(q) == 2:
                            cur.execute(q[0], q[1])
                        else:
                            cur.execute(q[0])
                    con.commit()
                    self.connection_pool.put((con, cur))
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                queries_str = '\n'.join([f'Query: {q[0]}\nParams: {q[1] if len(q)>1 else ""}' for q in query_params])
                raise Exception(f'{e}\n{queries_str}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            queries_str = '\n'.join([f'Query: {q[0]}\nParams: {q[1] if len(q)>1 else ""}' for q in query_params])
            raise Exception(f'{e}\n{queries_str}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            queries_str = '\n'.join([f'Query: {q[0]}\nParams: {q[1] if len(q)>1 else ""}' for q in query_params])
            raise Exception(f'{e}\n{queries_str}')
    
    def _excm(self, query, params):
        """
        Execute a parameterized query multiple times with different parameter sets.

        This internal method retrieves a connection from the pool, uses
        :meth:`MySQLdb.cursor.executemany` to execute the given query for each
        parameter tuple in the list, commits the transaction, and returns the
        connection to the pool. It is primarily used for bulk INSERT, UPDATE, or
        DELETE operations where many rows are affected with the same query pattern.

        If a connection error occurs (e.g., server gone away), the method attempts
        to handle it by discarding the broken connection, creating a new one, and
        retrying the entire operation. On any other SQL error, the transaction is
        rolled back and an exception is raised with detailed context.

        Args:
            query (str): The SQL query string containing placeholders (``%s``) for
                parameters. The same query is used for all executions.
            params (list of tuples or list of lists): A sequence of parameter
                sequences, where each inner sequence contains the values to
                substitute into the query for one execution.

        Returns:
            None: This method does not return any value; it only executes the query.

        Raises:
            Exception: If an operational or programming error occurs. The exception
                message includes the original error, the query, and the parameters
                to aid debugging.
            RuntimeError: If the connection pool is empty and a new connection cannot
                be created (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance ``db`` and a table ``users`` with columns
            ``name`` and ``age``::

                db._excm(
                    "INSERT INTO users (name, age) VALUES (%s, %s)",
                    [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
                )
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        con, cur = self._get_connection()
        try:
            cur.executemany(query, params)
            con.commit()
            self.connection_pool.put((con, cur))
        except OperationalError as e:
            if e.args[0] in self.CONNECTION_ERRORS:
                self._handle_broken_connection(con)
                con, cur = self._get_connection()
                try:
                    cur.executemany(query, params)
                    con.commit()
                    self.connection_pool.put((con, cur))
                except OperationalError:
                    self._handle_broken_connection(con)
                    raise
            else:
                con.rollback()
                self.connection_pool.put((con, cur))
                raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except ProgrammingError as e:
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
        except Exception as e:   # <--- اضافه کنید
            con.rollback()
            self.connection_pool.put((con, cur))
            raise Exception(f'{e}\nQuery:\n\t{query}\nParams:\n\t{params}')
    
    def _handle_broken_connection(self, con):
        """
        Handle a broken database connection by cleaning up and creating a replacement.

        This internal method is called when a connection error (e.g., server gone away)
        is detected. It attempts to close the broken connection, removes it from the
        internal connection storage list, and creates a new connection via
        :meth:`_create_connection` to replenish the pool. This ensures that the
        connection pool maintains the configured size even after failures.

        Args:
            con: The broken MySQL connection object (from `MySQLdb`). This connection
                is closed and discarded.

        Returns:
            None

        Raises:
            RuntimeError: If the driver has been disconnected (``_connected`` is
                ``False``) and :meth:`_create_connection` is called, this exception
                will propagate.

        Example:
            This method is typically used internally by query execution methods::

                try:
                    cursor.execute(query)
                except OperationalError as e:
                    if e.args[0] in self.CONNECTION_ERRORS:
                        self._handle_broken_connection(connection)
                        # Retry the query with a new connection
        """
        if not self._connected:
            raise RuntimeError("Driver disconnected")
        try:
            con.close()
        except:
            pass
        if con in self.connection_pool_storage:
            self.connection_pool_storage.remove(con)
        self._create_connection()

    def delete_table(self, table: Table, are_you_sure: bool, are_you_really_sure: bool, for_sure: bool):
        """
        Permanently drop the specified table from the database.

        This method executes a ``DROP TABLE`` SQL statement to delete the given table.
        To prevent accidental deletion, three separate confirmation flags must all be
        ``True``. After successful deletion, the table reference is also removed from
        the driver instance's attributes.

        Args:
            table (Table): The :class:`Table` object representing the table to delete.
            are_you_sure (bool): First confirmation flag; must be ``True`` to proceed.
            are_you_really_sure (bool): Second confirmation flag; must be ``True``.
            for_sure (bool): Third confirmation flag; must be ``True``.

        Returns:
            None: This method does not return a value.

        Raises:
            Exception: If the underlying SQL execution fails (e.g., table does not exist,
                permission denied). The exception is propagated from :meth:`Driver._exc`.

        Example:
            Assuming a ``Driver`` instance ``db`` and a table ``users`` already exists::

                db.delete_table(db.users, True, True, True)  # Deletes the 'users' table.

            The table reference ``db.users`` will no longer be available.

        Warning:
            This operation is irreversible. Ensure you have a backup or are certain
            before calling this method.
        """
        if are_you_sure and are_you_really_sure and for_sure:
            self._exc(f'DROP TABLE {table.name_};')
            self.__delattr__(table.name_[1:-1])

    def delete_database(self, database_name: str, are_you_sure: bool, are_you_really_sure: bool, for_sure: bool):
        """
        Permanently delete an entire MySQL database.

        This method executes a ``DROP DATABASE`` statement, which irreversibly removes
        the specified database and all its tables, data, and schema objects. To prevent
        accidental deletion, three explicit confirmation flags are required. All three
        must be ``True`` for the operation to proceed.

        Args:
            database_name (str): The name of the database to delete.
            are_you_sure (bool): First-level confirmation flag.
            are_you_really_sure (bool): Second-level confirmation flag.
            for_sure (bool): Final confirmation flag.

        Returns:
            None

        Raises:
            Exception: If any database error occurs (e.g., insufficient privileges,
                the database does not exist, or the connection fails). The exception
                message will include the original error and the query.

        Example:
            Assuming a ``Driver`` instance named ``db`` connected to a MySQL server::

                # Danger: this will delete the database 'old_data'
                db.delete_database('old_data', True, True, True)

            If any flag is ``False``, nothing happens::

                db.delete_database('old_data', True, True, False)  # No effect
        """
        if are_you_sure and are_you_really_sure and for_sure:
            self._exc(f'DROP DATABASE {database_name};')

    def custom_execute_with_fetch(self, query, params = None):
        """
        Execute a custom SQL query and return the fetched result set.

        This method provides a flexible way to run arbitrary SQL queries (e.g., SELECT)
        with optional parameter binding. It automatically chooses the appropriate
        internal execution method based on whether parameters are provided. If
        ``params`` is ``None``, the query is executed without parameters; otherwise,
        the query is executed with the given parameters. The method always fetches
        all rows and returns them as a list of tuples.

        Args:
            query (str): The SQL query to execute.
            params (list or tuple, optional): Parameter values to substitute into the
                query. If provided, the query should contain ``%s`` placeholders.
                Defaults to ``None``.

        Returns:
            list[tuple]: The fetched rows, where each row is represented as a tuple
                of column values. If the query returns no rows, an empty list is
                returned.

        Raises:
            Exception: If an operational or programming error occurs (e.g., syntax
                error, connection failure, or invalid parameters). The exception
                message includes the original error, the query, and the parameters
                (if any) for debugging.
            RuntimeError: If the connection pool is exhausted and a new connection
                cannot be created.

        Example:
            Assuming a ``Driver`` instance named ``db`` connected to a database
            with a table ``users``::

                # Execute a SELECT query without parameters
                rows = db.custom_execute_with_fetch("SELECT * FROM users")
                for row in rows:
                    print(row)

                # Execute a parameterized query
                rows = db.custom_execute_with_fetch(
                    "SELECT name FROM users WHERE age > %s",
                    [25]
                )
                print(rows)  # e.g., [('Alice',), ('Bob',)]
        """
        return self._excfp(query, params) if params else self._excf(query)
    
    def custom_execute(self, query: str, params: list = None) -> None:
        """
        Execute a custom SQL query with optional parameters.

        This method serves as a public wrapper around the internal execution methods
        :meth:`_exc` (for queries without parameters) and :meth:`_excp` (for queries
        with parameters). It automatically selects the appropriate method based on
        whether ``params`` are provided. The query is executed using a connection
        from the pool, committed, and the connection is returned to the pool.

        Args:
            query (str): The SQL query string to execute. If using parameters,
                use ``%s`` placeholders.
            params (list, optional): A list or tuple of parameter values to substitute
                into the query. Defaults to ``None``, in which case the query is
                executed without parameters.

        Returns:
            None: This method does not return any value; it only executes the query
            and commits the transaction.

        Raises:
            Exception: If an operational or programming error occurs. The exception
                message includes the original error, the query, and the parameters
                (if any) to aid debugging.
            RuntimeError: If the connection pool is empty and a new connection cannot
                be created (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance named ``db`` connected to a MySQL server
            with a table ``users``::

                # Execute a query without parameters
                db.custom_execute("DELETE FROM users WHERE age < 18")

                # Execute a parameterized query
                db.custom_execute(
                    "UPDATE users SET active = %s WHERE id = %s",
                    [False, 42]
                )
        """
        return self._excp(query, params) if params else self._exc(query)
    
    def custom_execute_many(self, query, params):
        """
        Execute the same parameterized SQL query multiple times with different parameter sets.

        This method is a wrapper around the internal :meth:`_excm` method, which uses
        ``cursor.executemany()`` for efficient bulk execution. It is ideal for batch
        inserts, updates, or deletes where the same query structure is repeated with
        varying data.

        Args:
            query (str): The SQL query string containing placeholders (``%s``) for
                parameters. The query must be compatible with ``executemany()``.
            params (list of tuple or list of list): A sequence of parameter sets.
                Each inner sequence provides values for the placeholders in the query.
                For example, ``[(1, 'Alice'), (2, 'Bob')]`` for a query like
                ``"INSERT INTO users (id, name) VALUES (%s, %s)"``.

        Returns:
            None: This method does not return a value. It executes the queries and
            commits the transaction upon success.

        Raises:
            Exception: If a database error occurs (e.g., connection issues, syntax
                errors, or constraint violations). The exception message includes
                the original error, the query, and the parameters to facilitate
                debugging.
            RuntimeError: If the connection pool is exhausted and a new connection
                cannot be established (indirectly via :meth:`_get_connection`).

        Example:
            Assuming a ``Driver`` instance named ``db`` and a table ``users``
            with columns ``id`` and ``name``::

                # Bulk insert multiple users
                db.custom_execute_many(
                    "INSERT INTO users (id, name) VALUES (%s, %s)",
                    [(1, 'Alice'), (2, 'Bob'), (3, 'Charlie')]
                )

                # Bulk update salaries
                db.custom_execute_many(
                    "UPDATE employees SET salary = salary * 1.1 WHERE id = %s",
                    [(101,), (102,), (103,)]
                )

            For large datasets, this method is significantly faster than calling
            :meth:`custom_execute` in a loop.
        """
        return self._excm(query, params)
    
    def get_databases(self):
        """
        Retrieve the list of all databases on the MySQL server.

        This method executes a ``SHOW DATABASES`` query and returns the names of
        all databases accessible by the current connection. The result is a flat
        list of database names.

        Returns:
            list[str]: A list of database names available on the server.

        Raises:
            Exception: If any database error occurs (e.g., connection lost,
                insufficient privileges). The exception message will include the
                original error and the executed query.

        Example:
            Assuming a connected ``Driver`` instance named ``db``::

                databases = db.get_databases()
                print(databases)  # e.g., ['information_schema', 'mysql', 'my_app_db']
        """
        return [i[0] for i in self._excf('SHOW DATABASES;')]
    
    def get_tables(self):
        """
        Retrieve the names of all tables in the currently selected database.

        This method executes a ``SHOW TABLES`` query and returns a list of table
        names as strings. It uses the internal :meth:`_excf` method to fetch the
        results.

        Returns:
            list[str]: A list of table names in the current database. If there are
            no tables, an empty list is returned.

        Raises:
            Exception: If a database error occurs (e.g., connection lost, insufficient
                privileges). The exception will include the original error message
                and the executed query.

        Example:
            Assuming a :class:`Driver` instance named ``db`` connected to a MySQL
            server::

                tables = db.get_tables()
                print(tables)  # e.g., ['users', 'orders', 'products']

            This method is automatically called during :class:`Driver` initialization
            to create :class:`Table` objects for each existing table.
        """
        return [i[0] for i in self._excf('SHOW TABLES;')]
    
    def create_table(self, table_structure: TableStructure):
        """
        Create a new database table based on the provided table structure.

        This method takes a :class:`TableStructure` object, retrieves the complete
        ``CREATE TABLE`` SQL statement via its :meth:`~TableStructure.get_structure`
        method, executes it on the database, and then dynamically adds the newly
        created table as an attribute on the driver instance (so it can be accessed
        as ``db.new_table``). The attribute is an instance of :class:`Table`
        representing the new table.

        Args:
            table_structure (TableStructure): A fully configured table structure
                object containing column definitions, constraints, foreign keys,
                and table options (charset, collate, etc.). Must have at least one
                column defined; otherwise, :meth:`~TableStructure.get_structure`
                raises an exception.

        Returns:
            None

        Raises:
            Exception: If the table structure has no columns (propagated from
                :meth:`~TableStructure.get_structure`).
            Exception: If a database error occurs during execution (e.g., table
                already exists, invalid data type, permission denied). The original
                error message and query are included in the exception.

        Example:
            Assuming a configured :class:`Driver` instance ``db`` and a
            :class:`TableStructure` object built for a ``users`` table::

                from ormophine.Mysql import TableStructure, DataTypes

                # Build the table structure
                users_table = (TableStructure('users')
                            .add_column('id', DataTypes.INT(), primary_key=True,
                                        auto_increment=True, not_null=True)
                            .add_column('username', DataTypes.VARCHAR(50),
                                        not_null=True, unique=True)
                            .add_column('email', DataTypes.VARCHAR(255))
                            .add_column('created_at', DataTypes.DATETIME(),
                                        default_value='CURRENT_TIMESTAMP'))

                # Create the table in the database
                db.create_table(users_table)

                # Now the table is available as an attribute
                db.users.insert({'username': 'alice', 'email': 'alice@example.com'})
        """
        self._exc(table_structure.get_structure())
        self.__setattr__(table_structure.name.strip('`'), Table(self, table_structure.name.strip('`')))

    def optimize(self):
        """
        Optimize and analyze all tables in the current database.

        This method iterates over all tables in the database (as returned by
        :meth:`get_tables`) and executes both ``OPTIMIZE TABLE`` and ``ANALYZE TABLE``
        on each. ``OPTIMIZE TABLE`` reclaims unused space and defragments the table
        data and indexes. ``ANALYZE TABLE`` updates table statistics to help the
        query optimizer make better execution plans. Running these operations
        regularly can improve database performance, especially after large data
        modifications.

        The operations are performed sequentially; if an error occurs on one table,
        the method may stop and raise an exception, leaving subsequent tables
        unprocessed.

        Args:
            None

        Returns:
            None

        Raises:
            Exception: If any database error occurs during the optimization or
                analysis of a table (e.g., table does not exist, permission denied,
                connection failure). The exception message includes the original
                error and the failing query.

        Example:
            Assuming a configured :class:`Driver` instance ``db`` connected to a
            database with tables ``users`` and ``orders``::

                # Perform maintenance on all tables
                db.optimize()

                # This will execute:
                # OPTIMIZE TABLE users;
                # ANALYZE TABLE users;
                # OPTIMIZE TABLE orders;
                # ANALYZE TABLE orders;
        """
        for i in self.get_tables():
            self._exc(f"OPTIMIZE TABLE `{i}`;")
            self._exc(f"ANALYZE TABLE `{i}`")

    def create_user(self, username: str, password: str, host: str = 'localhost'):
        """
        Create a new MySQL user account.

        This method executes a ``CREATE USER`` statement with the specified username,
        password, and host. The password is passed as a parameter to prevent SQL
        injection. The username and host are escaped by doubling single quotes to
        avoid syntax errors. If the user already exists or the current user lacks
        sufficient privileges, an exception is raised.

        Args:
            username (str): The username for the new account. Single quotes will be
                escaped automatically.
            password (str): The password for the new account. This is passed as a
                parameter, so it is safe from injection.
            host (str, optional): The host from which the user can connect. Defaults
                to ``'localhost'``.

        Returns:
            None

        Raises:
            Exception: If a database error occurs (e.g., user already exists,
                insufficient privileges, connection failure). The exception message
                includes the original error and the query for debugging.

        Example:
            Assuming a :class:`Driver` instance ``db`` connected to a MySQL server::

                # Create a user 'app_user' with password 'secure123' from localhost
                db.create_user('app_user', 'secure123')

                # Create a user 'remote_user' allowed to connect from any host
                db.create_user('remote_user', 'pass456', host='%')
        """
        for ch in (';', '--', '\0', "'"):
            if ch in username:
                raise Exception(f"Invalid username: '{username}' contains forbidden character '{ch}'")
        query = f"CREATE USER '{username.replace("'", "''")}'@'{host.replace("'", "''")}' IDENTIFIED BY %s;"
        self._excp(query, (password,))

    def drop_user(self, username: str, host: str = 'localhost'):
        """
        Permanently delete a MySQL user account.

        This method executes a ``DROP USER`` statement, which removes the specified
        user account from the MySQL server. The user account is identified by the
        combination of username and host. All privileges associated with the user
        are also revoked automatically.

        The method sanitizes the input by doubling single quotes (``'``) within
        the username and host to prevent SQL injection attacks. However, it is
        recommended to use parameterized queries for user-supplied input whenever
        possible.

        Args:
            username (str): The username of the account to drop. Single quotes
                within the string are automatically escaped.
            host (str, optional): The host part of the account (e.g., ``'localhost'``,
                ``'%'``, or a specific IP). Defaults to ``'localhost'``.

        Returns:
            None

        Raises:
            Exception: If the user does not exist, the current user lacks
                the ``DROP USER`` privilege, or a database error occurs. The
                original error message is included in the raised exception.

        Example:
            Assuming a configured :class:`Driver` instance named ``db``::

                # Drop user 'johndoe' at localhost
                db.drop_user('johndoe')

                # Drop user 'appuser' from any host
                db.drop_user('appuser', host='%')
        """
        query = f"DROP USER '{username.replace("'", "''")}'@'{host.replace("'", "''")}';"
        self._exc(query)

    def change_password(self, username: str, new_password: str, host: str = 'localhost'):
        """
        Change the password for an existing MySQL user account.

        This method executes an ``ALTER USER`` statement to update the password
        for the specified user at the given host. The password is passed as a
        parameter to prevent SQL injection. The change takes effect immediately
        for new connections; existing connections remain unaffected.

        Args:
            username (str): The name of the user whose password is to be changed.
            new_password (str): The new password for the user account.
            host (str, optional): The host part of the user account. Defaults to
                ``'localhost'``.

        Returns:
            None

        Raises:
            Exception: If the user does not exist, the current connection lacks
                sufficient privileges (e.g., ``CREATE USER`` or ``ALTER USER``
                privileges), or any other database error occurs. The exception
                message will contain the original error and the executed query.

        Example:
            Assuming a :class:`Driver` instance named ``db`` connected with
            administrative privileges::

                # Change password for 'app_user' on localhost
                db.change_password('app_user', 'new_secure_password_123')

                # Change password for a user on a specific host
                db.change_password('remote_user', 'p@ssw0rd', host='192.168.1.100')
        """
        query = f"ALTER USER '{username.replace("'", "''")}'@'{host.replace("'", "''")}' IDENTIFIED BY %s;"
        self._excp(query, (new_password,))

    def rename_user(self, old_username: str, old_host: str, new_username: str, new_host: str):
        """
        Rename an existing MySQL user account.

        This method executes a ``RENAME USER`` statement, which changes both the
        username and the host portion of an existing user account. Both the old and
        new host values must be specified, as MySQL identifies users by the
        combination of username and host. The method automatically escapes single
        quotes in the provided strings to prevent SQL injection.

        Args:
            old_username (str): The current username of the account to rename.
            old_host (str): The current host part of the account (e.g., ``'localhost'``
                or ``'%'``).
            new_username (str): The new username for the account.
            new_host (str): The new host part for the account.

        Returns:
            None

        Raises:
            Exception: If the ``RENAME USER`` statement fails (e.g., the old user
                does not exist, insufficient privileges, or the new user already
                exists). The exception includes the original error message and the
                generated query.

        Example:
            Assuming a :class:`Driver` instance ``db`` with appropriate privileges::

                # Rename user 'john'@'localhost' to 'jane'@'%'
                db.rename_user('john', 'localhost', 'jane', '%')

                # Rename user 'app_user'@'192.168.1.100' to 'prod_user'@'10.0.0.5'
                db.rename_user('app_user', '192.168.1.100', 'prod_user', '10.0.0.5')
        """
        query = f"RENAME USER '{old_username.replace("'", "''")}'@'{old_host.replace("'", "''")}' TO '{new_username.replace("'", "''")}'@'{new_host.replace("'", "''")}';"
        self._exc(query)

    def grant_privileges(self, username: str, host: str, privileges: PRIVILEGES, database: str, table: str = '*'):
        """
        Grant specific privileges on a database table to a MySQL user.

        This method constructs and executes a ``GRANT`` statement, allowing the
        specified user to perform the given operations on the target table. The
        privileges are granted immediately and take effect without requiring a
        flush (though the driver also provides :meth:`flush_privileges` if needed).

        Args:
            username (str): The name of the user to receive the privileges. Single
                quotes and special characters are automatically escaped.
            host (str): The host from which the user connects (e.g., ``'localhost'``
                or ``'%'``). Escaped automatically.
            privileges (PRIVILEGES): A privilege string from the driver's
                :attr:`PRIVILEGES` type literal (e.g., ``'SELECT'``, ``'ALL PRIVILEGES'``,
                ``'INSERT, UPDATE'``). Multiple privileges can be combined in a
                comma-separated string.
            database (str): The name of the database on which privileges are granted.
                Escaped automatically.
            table (str, optional): The table name within the database. Defaults to
                ``'*'``, meaning all tables in the database.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., user does not
                exist, invalid privilege name, insufficient permissions). The
                exception message includes the original error and the query.

        Example:
            Grant ``SELECT`` and ``INSERT`` on the ``users`` table to user
            ``'app_user'`` from localhost::

                db.grant_privileges(
                    username='app_user',
                    host='localhost',
                    privileges='SELECT, INSERT',
                    database='myapp',
                    table='users'
                )

            Grant all privileges on all tables in the database::

                db.grant_privileges(
                    username='admin',
                    host='%',
                    privileges='ALL PRIVILEGES',
                    database='myapp'
                )
        """
        query = f"GRANT {privileges} ON {database.replace("'", "''")}.{table.replace("'", "''")} TO '{username.replace("'", "''")}'@'{host.replace("'", "''")}';"
        self._exc(query)

    def revoke_privileges(self, username: str, host: str, privileges: PRIVILEGES, database: str, table: str = '*'):
        """
        Revoke specific privileges on a database table from a MySQL user.

        This method constructs and executes a ``REVOKE`` statement, removing the
        specified privileges from the given user on the target table. The changes
        take effect immediately; a subsequent :meth:`flush_privileges` is not
        required but can be called if needed.

        Args:
            username (str): The name of the user from whom to revoke privileges.
                Single quotes and special characters are automatically escaped.
            host (str): The host from which the user connects (e.g., ``'localhost'``
                or ``'%'``). Escaped automatically.
            privileges (PRIVILEGES): A privilege string from the driver's
                :attr:`PRIVILEGES` type literal (e.g., ``'SELECT'``, ``'ALL PRIVILEGES'``,
                ``'INSERT, UPDATE'``). Multiple privileges can be combined in a
                comma-separated string.
            database (str): The name of the database from which privileges are revoked.
                Escaped automatically.
            table (str, optional): The table name within the database. Defaults to
                ``'*'``, meaning all tables in the database.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., user does not
                exist, invalid privilege name, insufficient permissions). The
                exception message includes the original error and the query.

        Example:
            Revoke ``DELETE`` and ``UPDATE`` privileges on the ``users`` table from
            user ``'app_user'`` connecting from localhost::

                db.revoke_privileges(
                    username='app_user',
                    host='localhost',
                    privileges='DELETE, UPDATE',
                    database='myapp',
                    table='users'
                )

            Revoke all privileges on all tables in the database::

                db.revoke_privileges(
                    username='app_user',
                    host='%',
                    privileges='ALL PRIVILEGES',
                    database='myapp'
                )
        """
        query = f"REVOKE {privileges} ON {database.replace("'", "''")}.{table.replace("'", "''")} FROM '{username.replace("'", "''")}'@'{host.replace("'", "''")}';"
        self._exc(query)

    def flush_privileges(self):
        """
        Reload the MySQL privilege tables from the grant tables in the system database.

        This method executes the ``FLUSH PRIVILEGES`` statement, which forces MySQL
        to reload the privilege tables (stored in the ``mysql`` database) into memory.
        This is necessary after manually editing grant tables or when using privilege
        management statements like :meth:`grant_privileges` or :meth:`revoke_privileges`
        that do not automatically trigger a reload (though in most cases MySQL does
        it automatically). Calling this method ensures that all privilege changes
        take effect immediately for all active connections.

        Returns:
            None

        Raises:
            Exception: If the database execution fails (e.g., insufficient privileges
                to flush privileges, or a connection error). The exception message
                includes the original error and the query.

        Example:
            After granting or revoking privileges, you may explicitly flush::

                db.grant_privileges('app_user', 'localhost', 'SELECT', 'myapp')
                db.flush_privileges()  # Ensure the change is loaded
        """
        self._exc("FLUSH PRIVILEGES;")

    def disconnect(self):
        """
        Close all database connections and release resources.

        This method terminates the connection pool by closing every active MySQL
        connection stored in the pool, clearing the internal queue, and setting the
        connection status flag to ``False``. After calling this method, the driver
        instance cannot be used for further database operations; any attempt to
        execute a query or create a new connection will raise a :class:`RuntimeError`
        (via :meth:`_create_connection`). If you need to reconnect, you must create
        a new :class:`Driver` instance.

        The method attempts to close each connection, ignoring any errors that occur
        during the close process (e.g., connections already closed). It then drains
        the queue of any remaining connection objects.

        Args:
            None

        Returns:
            None

        Raises:
            None: While the method itself does not raise exceptions, subsequent
                database operations will fail with a :class:`RuntimeError` if
                attempted after disconnection.

        Example:
            Gracefully shut down the database connection pool::

                db = Driver(host='localhost', username='root', password='pass',
                            db_name='myapp')
                # ... perform operations ...
                db.disconnect()

            After disconnection, attempting to use the driver will fail::

                db.disconnect()
                db.get_tables()  # Raises RuntimeError: You have closed the connection...
        """
        if not self._connected:
            raise RuntimeError("Already disconnected")
        self._connected = False
        for i in self.connection_pool_storage:
            try:
                i.close()
            except:
                pass
        while not self.connection_pool.empty():
            self.connection_pool.get_nowait()



from __future__ import annotations
import re
from .. import Column, ColumnsOperation


class Builtins:
    """MySQL SQL function helpers that always return a :class:`ColumnsOperation`.

    ``Builtins`` is a stateless namespace of static methods, each of which
    wraps a MySQL function (or, where MySQL lacks a native function, a
    short composition of MySQL functions) into a :class:`ColumnsOperation`
    object. Because every helper returns a ``ColumnsOperation``, the result
    can be used anywhere a column expression is accepted: in the
    ``which_columns`` list of :meth:`Table.get_row`, in the ``where``
    condition, in the ``update`` dict of :meth:`Table.update`, inside
    :meth:`~Table.bulk_update`, and so on. Helpers can also be nested
    freely — ``Builtins.Round(Builtins.Avg(col) * 100) / 100`` compiles
    into a single SQL expression with all parameters collected in order.

    Design
    ------
    The class is deliberately a namespace, not an instance-based helper.
    You never write ``Builtins()``; every entry point is a ``@staticmethod``
    you call directly::

        from Ormophine.Mysql import Driver, Builtins

        rows = users.get_row(
            [users.id, Builtins.Len(users.username)],
            where=Builtins.Len(users.username) > 5,
        )

    All three of the standard operand types are accepted by every helper:

    - :class:`Column` — the fully qualified name (`` `table`.`col` ``) is
      embedded verbatim; no parameters are consumed.
    - :class:`ColumnsOperation` — the previously-built SQL fragment and its
      parameter list are reused and extended.
    - Any raw Python value (``str``, ``int``, ``float``, ``bytes``,
      ``None``, ``bool``) — bound as a ``%s`` placeholder. Binding rather
      than interpolating means the value never reaches the SQL text, so
      injection is impossible.

    Datatype propagation
    --------------------
    Each helper sets ``current_datatype`` on the returned
    ``ColumnsOperation`` to a Python type (``int``, ``float``, ``str``,
    ``bytes``, or ``None`` when the result's type depends on its branches,
    as in :meth:`IIf`). The downstream operator dispatcher in
    :class:`ColumnsOperation` uses this attribute to choose between
    arithmetic (``+``) and concatenation (``||``) when the expression is
    chained. Helpers therefore document their result type so that call
    sites can reason about chained expressions without trial and error.

    Coventional result types:

    ===================================  =========
    Helper family                        datatype
    ===================================  =========
    ``Len``, ``Find``, ``Sum``, ``Min``,
    ``Max``, ``Count``, ``Abs``, ``Sign``,
    ``Floor``, ``Ceil``, ``Int``, ``Bool``,
    ``DateDiffDays``, ``DateDiffSeconds``   ``int``
    ``Avg``, ``Round``, ``RoundTo``,
    ``Sqrt``, ``Pow``, ``Float``            ``float``
    ``Reverse``, ``Capitalize``, ``Str``,
    ``Date``, ``Time``, ``DateTime``,
    ``Now``, ``Today``, ``Strftime``,
    ``StrftimeMod``, ``Timediff``,
    ``DateAdd``, ``DateTimeAdd``,
    ``TimeAdd``                              ``str``
    ===================================  =========

    Date/time conventions
    ---------------------
    Date and time helpers return ``str`` in ISO-8601-like formats that
    MySQL produces natively (``'YYYY-MM-DD'``, ``'HH:MM:SS'``,
    ``'YYYY-MM-DD HH:MM:SS'``). They accept an ISO literal, a
    :class:`Column`, a :class:`ColumnsOperation`, or the special string
    ``'now'`` — the last is translated to ``CURDATE()``, ``CURTIME()``,
    ``NOW()``, or ``UNIX_TIMESTAMP()`` depending on the helper. This
    mirrors the calling convention of SQLite's ``date()``, ``time()`` and
    ``datetime()`` functions, so code ported from the SQLite counterpart
    of this ORM keeps working.

    SQLite-style modifiers such as ``'+1 day'``, ``'-3 hours'``,
    ``'start of month'`` are supported by :meth:`DateAdd`,
    :meth:`DateTimeAdd`, :meth:`TimeAdd` and :meth:`StrftimeMod`; they are
    folded into chained ``DATE_ADD``/``DATE_SUB`` calls (and, for the
    anchor modifiers, into ``DATE_FORMAT`` truncations) by the internal
    helper :meth:`_apply_modifiers`.

    Helpers that are intentionally absent
    -------------------------------------
    MySQL lacks a few functions that SQLite provides and that a
    drop-in port would otherwise include:

    - ``TYPEOF`` — MySQL uses ``information_schema`` for column metadata
      instead; see :meth:`Table.get_table_info`.
    - ``PRINTF`` / ``FORMAT`` — MySQL's ``FORMAT()`` only handles numbers;
      for general string formatting use :meth:`Str` combined with
      :meth:`ColumnsOperation.add_first` / :meth:`add_end`, or a raw
      ``custom_execute`` call.
    - ``TOTAL`` — SQLite's ``SUM`` that returns 0 instead of NULL over an
      empty group. In MySQL use :meth:`Sum` wrapped in
      ``Builtins.Func('IFNULL', Builtins.Sum(col), 0)``.
    - ``julianday`` — MySQL has ``TO_DAYS``, but the semantics differ and
      the ORM's date arithmetic helpers cover the same ground.

    Notes
    -----
    - The helpers do not cache or memoize; every call builds a fresh
      ``ColumnsOperation``. This is deliberate — the operations are cheap
      to construct and the alternative (mutating shared state) would break
      thread-safety.
    - The class is not instantiable for use; it exists only to group the
      static methods. Attempting ``Builtins()`` succeeds but produces an
      object with no useful behaviour.
    - All parameters collected by the helpers are appended to the
      ``_output[1]`` list in the order their placeholders appear in the
      SQL fragment, which is the order MySQLdb expects.

    Example:
        A quick tour of the helper categories::

            from Ormophine.Mysql import Driver, Builtins

            db    = Driver(host='localhost', port=3306, username='root',
                           password='secret', db_name='app')
            users = db.users

            # String
            Builtins.Len(users.username)
            Builtins.Reverse(users.username)
            Builtins.Find(users.email, '@')
            Builtins.Capitalize(users.name)

            # Aggregate
            Builtins.Sum(users.balance)
            Builtins.Avg(users.age)
            Builtins.Min(users.created_at)
            Builtins.Max(users.score)
            Builtins.Count('*')

            # Math
            Builtins.Abs(users.delta)
            Builtins.Round(users.price)
            Builtins.RoundTo(users.price, 2)
            Builtins.Floor(users.score)
            Builtins.Ceil(users.score)
            Builtins.Sign(users.delta)
            Builtins.Sqrt(users.x * users.x + users.y * users.y)
            Builtins.Pow(users.base, users.exp)

            # Type cast
            Builtins.Int(users.text_id)
            Builtins.Float(users.text_price)
            Builtins.Str(users.numeric_code)
            Builtins.Bool(users.flag)

            # Condition
            Builtins.IsNull(users.email)
            Builtins.IsNotNull(users.phone)
            Builtins.Between(users.age, 18, 65)
            Builtins.IIf(users.age >= 18, 'adult', 'minor')

            # Date and time
            Builtins.Year(users.created_at)
            Builtins.Month(users.created_at)
            Builtins.Day(users.created_at)
            Builtins.Hour(users.created_at)
            Builtins.Minute(users.created_at)
            Builtins.Second(users.created_at)
            Builtins.DayOfWeek(users.created_at)
            Builtins.DayOfYear(users.created_at)
            Builtins.WeekOfYear(users.created_at)
            Builtins.Weekday(users.created_at)
            Builtins.IsoWeekday(users.created_at)
            Builtins.Strftime('%Y-%m-%d', users.created_at)
            Builtins.Now()
            Builtins.Today()
            Builtins.UnixNow()
            Builtins.UnixEpoch(users.created_at)
            Builtins.DateAdd('now', '+1 day')
            Builtins.DateTimeAdd('now', '-1 hour')
            Builtins.TimeAdd('now', '+30 minutes')
            Builtins.StrftimeMod('%Y-%m', 'now', '-1 month')
            Builtins.DateDiffDays(Builtins.Now(), users.created_at)
            Builtins.DateDiffSeconds(Builtins.Now(), users.updated_at)
            Builtins.Timediff(Builtins.Now(), users.created_at)
    """

    class _NullCol:
        """Fallback stand-in used when a :class:`ColumnsOperation` has no originating :class:`Column`.

        Several helpers in :class:`Builtins` (e.g. :meth:`Builtins.Count`
        with ``'*'``, :meth:`Builtins.Now`, :meth:`Builtins.Today`,
        :meth:`Builtins.UnixNow`) produce SQL that does not refer to any
        particular table or column. For example ``COUNT(*)`` has no
        column operand, and ``NOW()`` is a server-side constant. But every
        :class:`ColumnsOperation` in this ORM is expected to carry a
        ``col_obj`` attribute pointing at the :class:`Column` that
        originated the expression — the operator dispatcher reads
        ``col_obj.datatype``, ``col_obj.name``, and
        ``col_obj.table_obj._PlaceHolder`` when deciding how to render a
        chained operation.

        Rather than passing ``None`` and forcing every call site to
        null-check, helpers with no natural column use an instance of
        ``_NullCol``. It supplies the same attribute surface as a real
        :class:`Column` so chained operations such as ``Builtins.Now() + 1``
        or ``Builtins.Count('*') * 2`` do not raise ``AttributeError``.

        Attributes:
            datatype (None): Always ``None``. Signals that the originating
                expression is not tied to a column type, so the operator
                dispatcher falls back to its safe defaults (arithmetic
                rather than concatenation when both sides are ambiguous).
            table_obj (None): Always ``None``. There is no parent table.
            name (str): Always the empty string. Substituting it into SQL
                would produce invalid SQL, but this path is never taken
                because the helper's own SQL fragment already contains
                the complete expression.
            first_name (str): Always the empty string. Same reasoning as
                ``name``.

        Example:
            Internal usage within a helper::

                @staticmethod
                def Now(_=None):
                    return Builtins._make('(NOW())', [], str, Builtins._NullCol)

            The ``Builtins._NullCol`` class itself (not an instance) is
            passed because the helpers only need the attribute surface,
            never any per-instance state. Passing the class works because
            attribute lookup falls through to the class-level definitions.
        """
        datatype   = None
        table_obj  = None
        name       = ''
        first_name = ''

    # ------------------------------------------------------------------ helpers

    @staticmethod
    def _normalize(value):
        """Coerce an operand into a uniform ``(sql, params, datatype, col_obj)`` tuple.

        This is the single point through which every :class:`Builtins`
        helper inspects its arguments. Because the three accepted operand
        types — :class:`Column`, :class:`ColumnsOperation`, and raw Python
        values — carry their SQL information in different shapes, the
        helpers cannot consume them directly; ``_normalize`` flattens each
        shape into a common four-element tuple so the helper body only
        ever deals with one representation.

        The returned tuple has the following layout:

        - ``sql`` (str): The SQL fragment representing the operand. For a
          :class:`Column` this is the fully qualified name
          (`` `table`.`col` ``); for a :class:`ColumnsOperation` it is the
          previously-built fragment; for a raw value it is the placeholder
          ``'%s'``.
        - ``params`` (list): The parameters that must be bound to the
          placeholders in ``sql``. Always a fresh list — mutating it does
          not affect the source operand.
        - ``datatype`` (type | None): The Python type that the operand
          evaluates to on the server side, used by the helper to set
          ``current_datatype`` on its result. ``None`` for a raw literal
          whose type is unknown (e.g. when ``value is None``).
        - ``col_obj`` (Column | type[_NullCol]): The originating column,
          or the :class:`_NullCol` class when there is none. Never
          ``None`` — helpers can rely on the attribute surface being
          present.

        Args:
            value: The operand to normalize. Accepted types:

                - :class:`ColumnsOperation` — its ``_output`` tuple is
                  unpacked; the parameter list is copied so the caller
                  cannot accidentally mutate the source operation.
                - :class:`Column` — the fully qualified name is used and
                  no parameters are produced. ``col_obj`` is the column
                  itself.
                - Any other Python value — wrapped as ``('%s', [value])``
                  with ``type(value)`` as the datatype and ``_NullCol``
                  as the column stand-in.

        Returns:
            tuple[str, list, type | None, Column | type[_NullCol]]: A
            four-element tuple ``(sql, params, datatype, col_obj)`` as
            described above. The ``params`` list is always freshly
            allocated.

        Example:
            Internal usage inside a helper — :meth:`Len` delegates the
            inspection to ``_normalize`` and then builds its own SQL::

                sql, p, _, c = Builtins._normalize(value)
                return Builtins._make(f'(LENGTH({sql}))', p, int, c)

            Behaviour on each operand type::

                >>> Builtins._normalize(users.username)
                ('`users`.`username`', [], str, <Column users.username>)

                >>> op = Builtins.Len(users.username)
                >>> Builtins._normalize(op)
                ('(LENGTH(`users`.`username`))', [], int, <Column users.username>)

                >>> Builtins._normalize('hello')
                ('%s', ['hello'], str, Builtins._NullCol)

                >>> Builtins._normalize(42)
                ('%s', [42], int, Builtins._NullCol)

                >>> Builtins._normalize(None)
                ('%s', [None], NoneType, Builtins._NullCol)
        """
        if isinstance(value, ColumnsOperation):
            raw    = value._output[1]
            params = list(raw) if isinstance(raw, list) else [raw]
            return (
                value._output[0],
                params,
                value.current_datatype,
                value.col_obj if value.col_obj is not None else Builtins._NullCol,
            )
        if isinstance(value, Column):
            return value.name, [], value.datatype, value
        return '%s', [value], type(value), Builtins._NullCol

    @staticmethod
    def _make(sql, params, datatype, col_obj):
        """Assemble a :class:`ColumnsOperation` from a raw SQL fragment and its metadata.

        Helpers build their SQL fragment by string concatenation (or by
        composing the fragments returned from :meth:`_normalize` of their
        operands), then hand the result to ``_make`` for packaging. The
        method creates a fresh ``ColumnsOperation`` **without** calling its
        ``__init__`` — it uses ``__new__`` instead — because the
        constructor initialises ``_output`` to ``('', [])`` and would
        overwrite the fragment we want to store. This is a deliberate
        optimisation: helpers are called far more often than the
        constructor, and skipping ``__init__`` avoids four attribute
        assignments per call.

        The returned object behaves exactly like any other
        ``ColumnsOperation``: it supports arithmetic, comparison, string
        methods, ``In``/``not_In``, ``If``, and so on. Its ``_output``
        tuple is the one passed in, and its ``current_datatype`` reflects
        the helper's declared result type.

        Args:
            sql (str): The complete SQL fragment for the expression,
                including any surrounding parentheses the helper chose to
                add. Must contain ``%s`` placeholders exactly as many times
                as ``params`` has elements.
            params (list): The parameter values to bind, in the order
                their placeholders appear in ``sql``. The list is stored
                by reference on the returned operation, so the caller
                should not retain and later mutate it.
            datatype (type | None): The Python type the expression
                evaluates to on the server. Used by the operator dispatcher
                when the operation is chained. Pass ``None`` when the type
                depends on runtime branches (e.g. :meth:`IIf`).
            col_obj (Column | type[_NullCol]): The originating column, or
                the :class:`_NullCol` class when there is none. Passing
                ``None`` is tolerated and treated as ``_NullCol``, but
                helpers should pass ``_NullCol`` explicitly for clarity.

        Returns:
            ColumnsOperation: A new operation whose ``_output`` is
            ``(sql, params)``, whose ``current_datatype`` is ``datatype``,
            and whose ``col_obj`` is ``col_obj`` (or ``_NullCol`` when
            ``col_obj is None``).

        Example:
            Internal usage inside a helper::

                # Len — MySQL LENGTH()
                return Builtins._make(f'(LENGTH({sql}))', p, int, c)

                # Now — no operands at all
                return Builtins._make('(NOW())', [], str, Builtins._NullCol)

                # IIf — datatype intentionally None because the branches
                # may disagree
                return Builtins._make(
                    f'(IF({cs}, {ts}, {es}))',
                    cp + tp + ep,
                    None,
                    c,
                )

            The result composes with the rest of the expression system::

                >>> op = Builtins._make('(NOW())', [], str, Builtins._NullCol)
                >>> op._output
                ('(NOW())', [])
                >>> op.current_datatype
                <class 'str'>
                >>> # Chain arithmetic — the dispatcher sees str, so it
                >>> # would pick || unless overridden by a cast builtin.
                >>> (Builtins.Int(Builtins.UnixNow()) + 60)._output[0]
                '((CAST((UNIX_TIMESTAMP()) AS SIGNED)) + %s)'
        """
        op = ColumnsOperation.__new__(ColumnsOperation)
        op._output          = (sql, params)
        op.col_obj          = col_obj if col_obj is not None else Builtins._NullCol
        op.current_datatype = datatype
        op._skip_post_fetch_str_conversion = False  
        return op

    @staticmethod
    def _is_now(value):
        """Detect the literal string ``'now'`` used as a date/time keyword.

        SQLite's ``date()``, ``time()`` and ``datetime()`` functions accept
        the literal string ``'now'`` as a keyword meaning "the current
        moment", and the same convention is preserved by the SQLite
        counterpart of this ORM. MySQL has no such keyword, so the
        date/time helpers (:meth:`Date`, :meth:`Time`, :meth:`DateTime`,
        :meth:`DateAdd`, :meth:`DateTimeAdd`, :meth:`TimeAdd`,
        :meth:`StrftimeMod`, :meth:`UnixEpoch`) check for ``'now'`` via
        this method and translate it into the appropriate MySQL
        zero-argument function:

        ===================  ===================
        Helper               MySQL translation
        ===================  ===================
        :meth:`Date`         ``CURDATE()``
        :meth:`Time`         ``CURTIME()``
        :meth:`DateTime`     ``NOW()``
        :meth:`DateAdd`      ``CURDATE()``
        :meth:`DateTimeAdd`  ``NOW()``
        :meth:`TimeAdd`      ``NOW()``
        :meth:`StrftimeMod`  ``NOW()``
        :meth:`UnixEpoch`    ``UNIX_TIMESTAMP()``
        ===================  ===================

        Matching is case-insensitive and ignores surrounding whitespace,
        so ``'NOW'``, ``'now'``, ``' Now '`` all trigger the translation.
        The comparison is only attempted against strings; any other type
        returns ``False`` without raising.

        Args:
            value: The candidate to test. Only a Python ``str`` can match;
                any other type returns ``False`` immediately.

        Returns:
            bool: ``True`` when ``value`` is a string whose stripped,
            lowercased form is exactly ``'now'``. ``False`` otherwise —
            including when ``value`` is ``'nowhere'``, ``'the now'``, or
            a :class:`Column` named ``'now'``.

        Example:
            Behaviour on typical inputs::

                >>> Builtins._is_now('now')
                True
                >>> Builtins._is_now('NOW')
                True
                >>> Builtins._is_now('  Now  ')
                True
                >>> Builtins._is_now('nowhere')
                False
                >>> Builtins._is_now(None)
                False
                >>> Builtins._is_now(42)
                False

            How a helper uses it — :meth:`Date` short-circuits to
            ``CURDATE()`` when the keyword is detected, before falling
            back to the general normalisation path::

                @staticmethod
                def Date(value):
                    if Builtins._is_now(value):
                        return Builtins._make('(CURDATE())', [], str,
                                              Builtins._NullCol)
                    sql, p, _, c = Builtins._normalize(value)
                    return Builtins._make(f'(DATE({sql}))', p, str, c)

            Users can therefore write the SQLite-style call
            ``Builtins.Date('now')`` alongside the MySQL-native
            ``Builtins.Today()`` and get the same result.
        """
        return isinstance(value, str) and value.strip().lower() == 'now'
    
    @staticmethod
    def Len(value):
        """Compute the length of a value using MySQL's ``LENGTH()`` function.

        Generates a SQL expression that returns the number of **bytes** in a
        string or binary value. This is the SQL analogue of Python's ``len()``
        for ASCII data, but MySQL counts bytes not characters — for
        ``utf8mb4`` text with multi-byte characters the result is larger than
        Python's ``len()`` would be.

        If you need character count, use ``CHAR_LENGTH`` via
        ``Builtins.Int(Builtins.Func(...))`` or a raw ``custom_execute``
        call. For pure-ASCII columns the two are equivalent.

        The result is always an ``INTEGER``, so it composes cleanly with
        comparisons and arithmetic.

        Args:
            value: The expression whose length is computed. Supported types:

                - :class:`ColumnsOperation` — the generated SQL fragment and
                  its parameters are reused.
                - :class:`Column` — the fully qualified column name is used
                  directly in the SQL; no parameters are consumed.
                - Any raw Python value (``str``, ``int``, ``float``,
                  ``bytes``) — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(LENGTH(<expr>))`` and whose ``current_datatype`` is ``int``.

        Example:
            Basic filtering — find users whose username is longer than 5
            characters::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.username],
                    where=Builtins.Len(users.username) > 5,
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE ((LENGTH(`users`.`username`)) > %s)
                # Parameters: [5]

            As a SELECT column — the length of each username::

                rows = users.get_row([
                    users.username,
                    Builtins.Len(users.username),
                ])
                # SELECT `users`.`username`, (LENGTH(`users`.`username`))
                # FROM `users`

            Chained with other operations — the length rounded up to a
            multiple of 4::

                padded_len = ((Builtins.Len(users.username) + 3) / 4) * 4
                rows = users.get_row(
                    [users.username, padded_len],
                    where=Builtins.Len(users.username) > 0,
                )
                # SELECT `users`.`username`,
                #        ((((LENGTH(`users`.`username`)) + %s) / %s) * %s)
                # FROM `users`
                # WHERE ((LENGTH(`users`.`username`)) > %s)
                # Parameters: [3, 4, 4, 0]

            Nested inside other builtins::

                avg_len = Builtins.Avg(Builtins.Len(users.username))
                rows = users.get_row([avg_len])
                # SELECT (AVG((LENGTH(`users`.`username`)))) FROM `users`

            Applied to a raw literal — the literal is bound as a parameter::

                rows = users.get_row([
                    Builtins.Len('hello'),    # -> (LENGTH(%s)) with param 'hello'
                    Builtins.Len(42),         # -> (LENGTH(%s)) with param 42
                ])

            Filter inside a compound condition::

                active_short = Builtins.Sum(
                    Builtins.IIf(
                        (Builtins.Len(users.username) < 8)
                        & (users.active == 1),
                        1,
                        0,
                    )
                )
                rows = users.get_row([active_short])
                # SELECT (SUM((IF(
                #   (((LENGTH(`users`.`username`)) < %s)
                #    AND (`users`.`active` = %s)),
                #   %s, %s))))
                # FROM `users`
                # Parameters: [8, 1, 1, 0]
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(LENGTH({sql}))', p, int, c)

    @staticmethod
    def Reverse(value):
        """Reverse the characters of a text value using MySQL's ``REVERSE()``.

        Generates a ``REVERSE(<expr>)`` expression. The result is a new
        string with the same characters in the opposite order. This is the
        SQL analogue of Python's ``s[::-1]`` idiom.

        The result is always a ``TEXT`` string, so ``+`` chained after it
        produces concatenation (``||``) rather than arithmetic addition —
        the driver enables ``PIPES_AS_CONCAT`` automatically.

        .. note::
            ``REVERSE()`` is available in MySQL 4.1+ and in all MariaDB
            versions, so it works on every supported backend. It operates
            on bytes, so the reversed form of a multi-byte string may not
            be valid text — this only matters for exotic data.

        Args:
            value: The expression whose characters are reversed. Supported
                types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(REVERSE(<expr>))`` and whose ``current_datatype`` is always
            ``str``.

        Example:
            Reverse every username as a simple pseudo-hash::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([
                    users.id,
                    users.username,
                    Builtins.Reverse(users.username),
                ])
                # SELECT `users`.`id`, `users`.`username`,
                #        (REVERSE(`users`.`username`))
                # FROM `users`
                # -> e.g. [(1, 'alice', 'ecila'), (2, 'bob', 'bob'), ...]

            Palindrome check — a string equals its own reverse::

                is_palindrome = users.username == Builtins.Reverse(users.username)
                rows = users.get_row(
                    [users.username],
                    where=is_palindrome,
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE (`users`.`username` = (REVERSE(`users`.`username`)))

            Suffix search turned into prefix search::

                rows = users.get_row(
                    [users.username],
                    where=Builtins.Reverse(users.username).startswith('e'),
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE ((REVERSE(`users`.`username`)) LIKE %s || '%')
                # Parameters: ['e']

            Sort by reversed string — clusters anagrams::

                rows = users.get_row(
                    [users.username],
                    order_by=Builtins.Reverse(users.username),
                )

            Chain with other string builtins::

                expr = Builtins.Reverse(Builtins.Upper(users.username))
                # -> (REVERSE((UPPER(`users`.`username`))))
                rows = users.get_row([expr])

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Reverse('hello'),    # -> (REVERSE(%s)) with param 'hello'
                ])
                # -> [('olleh',)]
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(REVERSE({sql}))', p, str, c)

    @staticmethod
    def Find(value, sub):
        """Locate a substring within a value using MySQL's ``INSTR()`` function.

        Generates an ``INSTR(<value>, <sub>)`` expression. Returns the
        1-based position of the first occurrence of ``sub`` inside
        ``value``, or ``0`` if ``sub`` is not present.

        .. warning::
            MySQL's ``INSTR`` and Python's ``str.find`` differ in two ways
            that routinely trip people up:

            ==================  =========  =========
            Behaviour           Python     MySQL
            ==================  =========  =========
            Index of first char    0          1
            "Not found" value     -1          0
            ==================  =========  =========

            If you need strict Python semantics — e.g. so that
            ``Find(col, 'x') >= 0`` means "contains x" — add ``- 1`` to the
            result at the call site::

                py_find = Builtins.Find(users.name, 'x') - 1
                # py_find == -1 when not found, == 0 when found at position 0

            For a plain "does it contain this" check, :meth:`contains` on
            :class:`ColumnsOperation` is usually the better choice.

        Args:
            value: The haystack. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

            sub: The needle. Same accepted types as ``value``. Usually a
                plain string literal, but any expression is allowed.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(INSTR(<value>, <sub>))`` and whose ``current_datatype`` is
            ``int``. Parameters are concatenated left-to-right: first the
            haystack's parameters, then the needle's.

        Example:
            Find the position of a delimiter inside a text column::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='app')
                emails = db.emails

                at_pos = Builtins.Find(emails.address, '@')
                rows = emails.get_row([
                    emails.id,
                    emails.address,
                    at_pos,
                ])
                # SELECT `emails`.`id`, `emails`.`address`,
                #        (INSTR(`emails`.`address`, %s))
                # FROM `emails`
                # Parameters: ['@']
                # -> e.g. [(1, 'alice@example.com', 6), (2, 'bob@x.io', 4), ...]

            Detect the presence of a substring — ``> 0`` because MySQL
            returns 0 (not -1) when the needle is missing::

                rows = emails.get_row(
                    [emails.id, emails.address],
                    where=Builtins.Find(emails.address, 'spam') > 0,
                )
                # SELECT ... WHERE ((INSTR(`emails`.`address`, %s)) > %s)
                # Parameters: ['spam', 0]

            Python-equivalent "not found" check — add ``- 1`` so -1 means
            "missing"::

                py_find = Builtins.Find(emails.address, 'example') - 1
                rows = emails.get_row(
                    [emails.address],
                    where=py_find == -1,
                )
                # SELECT `emails`.`address` FROM `emails`
                # WHERE (((INSTR(`emails`.`address`, %s)) - %s) = %s)
                # Parameters: ['example', 1, -1]

            Use a column as the needle — find where one column appears
            inside another::

                rows = emails.get_row([
                    emails.id,
                    Builtins.Find(emails.address, emails.username),
                ])
                # SELECT `emails`.`id`,
                #        (INSTR(`emails`.`address`, `emails`.`username`))
                # FROM `emails`

            Combine with slicing to extract everything before the '@'::

                at_pos = Builtins.Find(emails.address, '@')
                local_part = emails.address[:at_pos - 1]
                rows = emails.get_row([emails.id, local_part])
        """
        s1, p1, _, c = Builtins._normalize(value)
        s2, p2, _, _ = Builtins._normalize(sub)
        return Builtins._make(f'(INSTR({s1}, {s2}))', p1 + p2, int, c)

    @staticmethod
    def Capitalize(value):
        """Capitalise a text value — first character upper, rest lower.

        MySQL has no native ``CAPITALIZE()`` function, so this helper
        composes one from two SQL expressions:

        .. code-block:: sql

            CONCAT(UPPER(SUBSTRING(<expr>, 1, 1)), LOWER(SUBSTRING(<expr>, 2)))

        The result is a new string with the first character converted to
        uppercase and every subsequent character converted to lowercase.
        This matches Python's ``str.capitalize()`` behaviour for the
        common case of an all-alphabetic ASCII input. Edge cases differ:
        Python treats non-ASCII letters specially, and both Python and
        this helper leave digits and punctuation unchanged.

        The result is always a ``TEXT`` string, so ``+`` chained after it
        produces concatenation (``||``) rather than arithmetic addition.

        Args:
            value: The expression whose first character is capitalised.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused. The fragment is embedded twice in
                  the output (once for the first char, once for the rest),
                  so any parameters it carries are also duplicated.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string — bound as a ``%s`` placeholder
                  (also duplicated).

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CONCAT(UPPER(SUBSTRING(<expr>, 1, 1)), LOWER(SUBSTRING(<expr>, 2))))``
            and whose ``current_datatype`` is always ``str``.

        Example:
            Title-case names for display::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([
                    users.id,
                    Builtins.Capitalize(users.name),
                ])
                # SELECT `users`.`id`,
                #        (CONCAT(UPPER(SUBSTRING(`users`.`name`, 1, 1)),
                #                LOWER(SUBSTRING(`users`.`name`, 2))))
                # FROM `users`
                # -> e.g. [(1, 'Alice'), (2, 'Bob'), (3, 'Carol'), ...]
                #    even if the stored values were 'ALICE', 'bob', 'cAROL'

            Sort case-insensitively by name::

                rows = users.get_row(
                    [users.name],
                    order_by=Builtins.Capitalize(users.name),
                )

            Normalise before grouping::

                initial = Builtins.Capitalize(users.name)
                rows = users.get_row(
                    [initial],
                )
                from collections import Counter
                # Counter on the fetched values shows how many names start
                # with each capital letter.

            Chain with other string builtins::

                expr = Builtins.Capitalize(users.name).add_end('!')
                rows = users.get_row([expr])
                # -> 'Alice!', 'Bob!', ...

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Capitalize('hELLO wORLD'),   # -> 'Hello world'
                ])
                # SQL: (CONCAT(UPPER(SUBSTRING(%s, 1, 1)), LOWER(SUBSTRING(%s, 2))))
                # Parameters: ['hELLO wORLD', 'hELLO wORLD']

            Combine with Trim to clean up and capitalise in one go::

                clean = Builtins.Capitalize(users.name.strip())
                rows = users.get_row([clean])
        """
        sql, p, _, c = Builtins._normalize(value)
        inner = f'CONCAT(UPPER(SUBSTRING({sql}, 1, 1)), LOWER(SUBSTRING({sql}, 2)))'
        return Builtins._make(f'({inner})', p + p, str, c)   

    @staticmethod
    def Sum(value):
        """Compute the sum of a set of values using MySQL's ``SUM()`` aggregate.

        Generates a SQL ``SUM(<expr>)`` expression. When used in a SELECT
        list without a ``GROUP BY`` clause, it aggregates over all rows
        returned by the query. When used inside a ``WHERE`` clause it has
        no meaning on its own (MySQL will reject it) — ``Sum`` is meant
        for the SELECT side of a query, not for filtering.

        The resulting ``current_datatype`` is propagated from the input:

        - ``int``   → ``int`` (MySQL keeps integer sums as integers unless
          the column is ``UNSIGNED BIGINT``, which is widened to
          ``DECIMAL``)
        - ``float`` → ``float``
        - anything else → ``int`` (conservative default)

        This keeps arithmetic chains (``Builtins.Sum(col) + 1``) compiling
        to ``+`` and not ``||``.

        .. note::
            ``SUM`` returns ``NULL`` over an empty group. If you want 0
            instead, wrap in ``IFNULL`` via
            ``Builtins.IfNull(Builtins.Sum(col), 0)`` or use
            ``Builtins.Func('COALESCE', ...)``.

        Args:
            value: The expression to sum. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(SUM(<expr>))`` and whose ``current_datatype`` matches the
            input's numeric type (or ``int`` if the input is not numeric).

        Example:
            Total salary across all employees::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                total = Builtins.Sum(employees.salary)
                rows  = employees.get_row([total])
                # SELECT (SUM(`employees`.`salary`)) FROM `employees`
                # -> [(1245000,)] for example

            Sum alongside other aggregates::

                rows = employees.get_row([
                    Builtins.Count(employees.id),
                    Builtins.Sum(employees.salary),
                    Builtins.Avg(employees.salary),
                ])
                # SELECT (COUNT(`employees`.`id`)),
                #        (SUM(`employees`.`salary`)),
                #        (AVG(`employees`.`salary`))
                # FROM `employees`

            Sum combined with arithmetic (stays numeric, not string)::

                with_bonus = Builtins.Sum(employees.salary) + 10000
                rows = employees.get_row([with_bonus])
                # SELECT ((SUM(`employees`.`salary`)) + %s) FROM `employees`
                # Parameters: [10000]

            Sum with a filter — combine with ``where=`` in the usual way::

                rows = employees.get_row(
                    [Builtins.Sum(employees.salary)],
                    where=employees.department == 'Engineering',
                )
                # SELECT (SUM(`employees`.`salary`)) FROM `employees`
                # WHERE (`employees`.`department` = %s)
                # Parameters: ['Engineering']

            Conditional aggregate — count how many rows satisfy a
            condition inside a single SUM::

                adults = Builtins.Sum(
                    Builtins.IIf(employees.age >= 18, 1, 0)
                )
                rows = employees.get_row([adults])
                # SELECT (SUM((IF((`employees`.`age` >= %s), %s, %s))))
                # FROM `employees`
                # Parameters: [18, 1, 0]
        """
        sql, p, dt, c = Builtins._normalize(value)
        result_dt = dt if dt in (int, float) else int
        return Builtins._make(f'(SUM({sql}))', p, result_dt, c)

    @staticmethod
    def Avg(value):
        """Compute the average of a set of values using MySQL's ``AVG()`` aggregate.

        Generates a SQL ``AVG(<expr>)`` expression. MySQL always returns
        the average as a ``DECIMAL`` value, even when every input value is
        an integer; therefore ``current_datatype`` is unconditionally set
        to ``float`` regardless of the input's datatype.

        Like :meth:`Sum`, this aggregate is meant for the SELECT side of a
        query. Using it in a ``WHERE`` clause will raise an SQL error
        unless the query contains a ``GROUP BY`` that makes the aggregate
        legal.

        .. note::
            ``AVG`` returns ``NULL`` over an empty group. Handle the empty
            case in Python (``if avg is None: ...``) or wrap in
            ``Builtins.IfNull(Builtins.Avg(col), 0)``.

        Args:
            value: The expression to average. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(AVG(<expr>))`` and whose ``current_datatype`` is always
            ``float``.

        Example:
            Average salary across all employees::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                rows = employees.get_row([Builtins.Avg(employees.salary)])
                # SELECT (AVG(`employees`.`salary`)) FROM `employees`
                # -> [(78750.0,)] for example

            Average rounded to two decimals::

                rows = employees.get_row([
                    Builtins.Round(
                        Builtins.Avg(employees.salary) * 100
                    ) / 100
                ])
                # SELECT ((ROUND(((AVG(`employees`.`salary`)) * %s))) / %s)
                # FROM `employees`
                # Parameters: [100, 100]

            Average of a computed expression::

                rows = employees.get_row([
                    Builtins.Avg(employees.salary + employees.bonus)
                ])
                # SELECT (AVG((`employees`.`salary` + `employees`.`bonus`)))
                # FROM `employees`

            Combined with other aggregates in a single row::

                rows = employees.get_row([
                    Builtins.Count(employees.id),
                    Builtins.Avg(employees.salary),
                    Builtins.Sum(employees.salary),
                ])

            Average with a filter — average salary of one department::

                rows = employees.get_row(
                    [Builtins.Avg(employees.salary)],
                    where=employees.department == 'Engineering',
                )
                # SELECT (AVG(`employees`.`salary`)) FROM `employees`
                # WHERE (`employees`.`department` = %s)
                # Parameters: ['Engineering']

            Comparing an AVG result in Python after fetching::

                rows = employees.get_row(
                    [employees.department, Builtins.Avg(employees.salary)],
                )
                above_average = [d for d, avg in rows if avg > 60000]
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(AVG({sql}))', p, float, c)

    @staticmethod
    def Min(value):
        """Compute the minimum of a set of values using MySQL's ``MIN()`` aggregate.

        Generates a SQL ``MIN(<expr>)`` expression. When used with a
        single argument in the SELECT list, ``MIN`` acts as an aggregate
        over all rows. (When used with multiple arguments it acts as a
        scalar function, but this method only accepts one argument — for
        the scalar form use ``Builtins.Func('LEAST', a, b, c)`` instead.)

        The resulting ``current_datatype`` is propagated from the input,
        because MySQL returns the same type as the operand:

        - ``int``   → ``int``
        - ``float`` → ``float``
        - ``str``   → ``str`` (lexicographic minimum)
        - ``bytes`` → ``bytes``

        This keeps downstream concatenation (``Builtins.Min(col) + '!'``)
        compiling to ``||`` when the input was text.

        Args:
            value: The expression whose minimum is computed. Supported
                types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(MIN(<expr>))`` and whose ``current_datatype`` matches the
            input's datatype.

        Example:
            Lowest salary in the table::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                rows = employees.get_row([Builtins.Min(employees.salary)])
                # SELECT (MIN(`employees`.`salary`)) FROM `employees`
                # -> [(45000,)] for example

            Earliest signup date::

                users = db.users
                rows  = users.get_row([Builtins.Min(users.created_at)])
                # SELECT (MIN(`users`.`created_at`)) FROM `users`

            Lexicographic minimum of a text column::

                rows = employees.get_row([Builtins.Min(employees.name)])
                # current_datatype == str, so chaining works:
                expr = Builtins.Min(employees.name) + ' (first alphabetically)'
                # SELECT ((MIN(`employees`.`name`)) || %s) FROM `employees`
                # Parameters: [' (first alphabetically)']

            Minimum of a computed expression::

                rows = employees.get_row([
                    Builtins.Min(employees.salary - employees.bonus)
                ])

            Minimum with a filter::

                rows = employees.get_row(
                    [Builtins.Min(employees.salary)],
                    where=employees.department == 'Sales',
                )
                # SELECT (MIN(`employees`.`salary`)) FROM `employees`
                # WHERE (`employees`.`department` = %s)
                # Parameters: ['Sales']
        """
        sql, p, dt, c = Builtins._normalize(value)
        return Builtins._make(f'(MIN({sql}))', p, dt, c)

    @staticmethod
    def Max(value):
        """Compute the maximum of a set of values using MySQL's ``MAX()`` aggregate.

        Generates a SQL ``MAX(<expr>)`` expression. The mirror image of
        :meth:`Min`: same typing rules, same aggregate semantics, same
        single-argument signature.

        The resulting ``current_datatype`` is propagated from the input so
        that arithmetic and concatenation chains compile to the correct
        SQL operator.

        Args:
            value: The expression whose maximum is computed. Supported
                types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(MAX(<expr>))`` and whose ``current_datatype`` matches the
            input's datatype.

        Example:
            Highest salary in the table::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                rows = employees.get_row([Builtins.Max(employees.salary)])
                # SELECT (MAX(`employees`.`salary`)) FROM `employees`
                # -> [(180000,)] for example

            Latest signup date::

                users = db.users
                rows  = users.get_row([Builtins.Max(users.created_at)])
                # SELECT (MAX(`users`.`created_at`)) FROM `users`

            Combined with the id of the top earner — get both at once::

                rows = employees.get_row([
                    Builtins.Max(employees.salary),
                    employees.id,
                ])
                # The first value is the max salary; the second is the id
                # of an arbitrary row (MySQL picks one) — for a strict
                # "id of the top earner" use order_by + limit:
                top = employees.get_row(
                    [employees.id, employees.name, employees.salary],
                    order_by=employees.salary * -1,   # descending
                    limit=1,
                )

            Maximum of a computed expression::

                rows = employees.get_row([
                    Builtins.Max(employees.salary + employees.bonus)
                ])

            Maximum with a filter::

                rows = employees.get_row(
                    [Builtins.Max(employees.salary)],
                    where=employees.department == 'Sales',
                )
                # SELECT (MAX(`employees`.`salary`)) FROM `employees`
                # WHERE (`employees`.`department` = %s)
                # Parameters: ['Sales']
        """
        sql, p, dt, c = Builtins._normalize(value)
        return Builtins._make(f'(MAX({sql}))', p, dt, c)

    @staticmethod
    def Count(value):
        """Count rows using MySQL's ``COUNT()`` aggregate.

        Two forms are supported, mirroring MySQL's own behaviour:

        * **Row count** — when ``value`` is the literal string ``'*'``,
          generates ``COUNT(*)`` which counts every row in the group,
          regardless of NULL values.
        * **Non-null count** — for any other input, generates
          ``COUNT(<expr>)`` which counts only the rows where the
          expression evaluates to a non-NULL value.

        The result is always an ``INTEGER``, regardless of the input's
        datatype.

        Args:
            value: What to count. Supported types:

                - The string ``'*'`` — produces ``COUNT(*)`` and no
                  parameters.
                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused; produces ``COUNT(<fragment>)``.
                - :class:`Column` — the fully qualified column name is
                  used; produces ``COUNT(`table`.`column`)``.
                - Any other raw Python value — bound as a ``%s``
                  placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is either
            ``(COUNT(*))`` or ``(COUNT(<expr>))`` and whose
            ``current_datatype`` is always ``int``.

        Example:
            Count all rows in the table::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                rows = employees.get_row([Builtins.Count('*')])
                # SELECT (COUNT(*)) FROM `employees`
                # -> [(250,)] for example

            Count rows where a column is non-NULL::

                users = db.users
                rows  = users.get_row([Builtins.Count(users.email)])
                # SELECT (COUNT(`users`.`email`)) FROM `users`
                # -> [(198,)] — 52 users have no email on file

            Count with a filter — combine with where= in the usual way::

                rows = users.get_row(
                    [Builtins.Count('*')],
                    where=users.is_active == 1,
                )
                # SELECT (COUNT(*)) FROM `users`
                # WHERE (`users`.`is_active` = %s)
                # Parameters: [1]

            Count of a computed expression::

                rows = users.get_row([
                    Builtins.Count(Builtins.Upper(users.username))
                ])
                # SELECT (COUNT((UPPER(`users`.`username`)))) FROM `users`

            Boolean count via IF — count how many rows satisfy a
            condition inside a single aggregate::

                adults = Builtins.Sum(
                    Builtins.IIf(users.age >= 18, 1, 0)
                )
                rows = users.get_row([adults])
                # SELECT (SUM((IF((`users`.`age` >= %s), %s, %s))))
                # FROM `users`
                # Parameters: [18, 1, 0]
        """
        if isinstance(value, str) and value == '*':
            return Builtins._make('(COUNT(*))', [], int, Builtins._NullCol)
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(COUNT({sql}))', p, int, c)

    # ==================================================================== math

    @staticmethod
    def Abs(value):
        """Compute the absolute value of a number using MySQL's ``ABS()`` function.

        Generates a SQL ``ABS(<expr>)`` expression. The return type mirrors
        the input's numeric type:

        - ``int``   → ``int``
        - ``float`` → ``float``
        - anything else → ``float`` (conservative default, since ``ABS``
          only makes sense for numeric inputs)

        This keeps arithmetic chains (``Builtins.Abs(col) + 1``) compiling
        to ``+`` rather than ``||``.

        Args:
            value: The expression whose absolute value is computed.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(ABS(<expr>))`` and whose ``current_datatype`` matches the
            input's numeric type (or ``float`` if the input is not
            numeric).

        Example:
            Magnitude of a signed delta::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='analytics')
                movements = db.movements

                rows = movements.get_row([
                    movements.account_id,
                    Builtins.Abs(movements.delta),
                ])
                # SELECT `movements`.`account_id`,
                #        (ABS(`movements`.`delta`))
                # FROM `movements`

            Filter by absolute value::

                rows = movements.get_row(
                    [movements.account_id, movements.delta],
                    where=Builtins.Abs(movements.delta) > 1000,
                )
                # SELECT `movements`.`account_id`, `movements`.`delta`
                # FROM `movements`
                # WHERE ((ABS(`movements`.`delta`)) > %s)
                # Parameters: [1000]

            Absolute value of a computed expression::

                balance_change = movements.credit - movements.debit
                rows = movements.get_row([Builtins.Abs(balance_change)])
                # SELECT (ABS((`movements`.`credit` - `movements`.`debit`)))
                # FROM `movements`

            Chained with arithmetic — stays numeric::

                padded = Builtins.Abs(movements.delta) + 1
                # SELECT ((ABS(`movements`.`delta`)) + %s) FROM `movements`

            Applied to a raw literal::

                rows = movements.get_row([
                    Builtins.Abs(-42),    # -> (ABS(%s)) with param -42
                    Builtins.Abs(-3.14),  # -> (ABS(%s)) with param -3.14
                ])
        """
        sql, p, dt, c = Builtins._normalize(value)
        result_dt = dt if dt in (int, float) else float
        return Builtins._make(f'(ABS({sql}))', p, result_dt, c)

    @staticmethod
    def Round(value):
        """Round a number to the nearest integer using MySQL's ``ROUND()`` function.

        Generates a SQL ``ROUND(<expr>)`` expression. MySQL's single-arg
        ``ROUND`` rounds to zero decimal places. Unlike SQLite, MySQL's
        ``ROUND`` returns the **same type as the input** when possible:
        ``ROUND(INTEGER)`` returns an INTEGER, ``ROUND(DECIMAL)`` returns
        a DECIMAL, ``ROUND(DOUBLE)`` returns a DOUBLE. Because we cannot
        know the underlying type reliably, ``current_datatype`` is set to
        ``float`` — the safest choice for downstream arithmetic.

        To round to a specific number of decimal places, use
        :meth:`RoundTo` or wrap with ``Builtins.Func('ROUND', value, n)``.

        Args:
            value: The expression whose value is rounded. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(ROUND(<expr>))`` and whose ``current_datatype`` is always
            ``float``.

        Example:
            Round a computed price to the nearest integer::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='store')
                products = db.products

                rows = products.get_row([
                    products.name,
                    Builtins.Round(products.price * 1.07),
                ])
                # SELECT `products`.`name`,
                #        (ROUND((`products`.`price` * %s)))
                # FROM `products`
                # Parameters: [1.07]

            Round then cast to integer::

                rows = products.get_row([
                    Builtins.Int(Builtins.Round(products.price)),
                ])
                # SELECT (CAST((ROUND(`products`.`price`)) AS SIGNED))
                # FROM `products`

            Round an aggregate::

                rows = products.get_row([
                    Builtins.Round(Builtins.Avg(products.price) * 100) / 100,
                ])
                # Two-decimal average price:
                # SELECT ((ROUND(((AVG(`products`.`price`)) * %s))) / %s)
                # FROM `products`
                # Parameters: [100, 100]

            Filter by rounded value::

                rows = products.get_row(
                    [products.name, products.price],
                    where=Builtins.Round(products.price) > 100,
                )
                # SELECT ... WHERE ((ROUND(`products`.`price`)) > %s)
                # Parameters: [100]

            Chained with arithmetic — ``current_datatype`` is float, so
            arithmetic stays numeric::

                padded = Builtins.Round(products.price) * 1.1
                # SELECT ((ROUND(`products`.`price`)) * %s) FROM `products`
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(ROUND({sql}))', p, float, c)

    @staticmethod
    def RoundTo(value, decimals):
        """Round a number to a fixed number of decimal places using ``ROUND(x, n)``.

        Generates ``ROUND(<expr>, <n>)``. The second argument is a plain
        integer literal (not a parameterised value) because MySQL requires
        it to be a constant at query-parse time. Rounds half away from
        zero, matching MySQL's default rounding mode.

        Use this instead of composing ``Round(value * 100) / 100`` when
        you want MySQL's native, precise decimal rounding rather than
        floating-point tricks.

        Args:
            value: The expression whose value is rounded. Supported types
                are the same as for :meth:`Round`.
            decimals (int): Number of digits after the decimal point.
                Negative values round to the left of the decimal point
                (``RoundTo(123.45, -1)`` → ``120``).

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(ROUND(<expr>, <n>))`` and whose ``current_datatype`` is
            ``float``.

        Example:
            Two-decimal prices::

                rows = products.get_row([
                    products.name,
                    Builtins.RoundTo(products.price, 2),
                ])
                # SELECT `products`.`name`, (ROUND(`products`.`price`, 2))
                # FROM `products`

            Round to hundreds::

                rows = products.get_row([
                    Builtins.RoundTo(products.revenue, -2),
                ])
                # SELECT (ROUND(`products`.`revenue`, -2)) FROM `products`

            Average with two decimals — a cleaner alternative to
            ``Round(Avg(x) * 100) / 100``::

                rows = products.get_row([
                    Builtins.RoundTo(Builtins.Avg(products.price), 2),
                ])
                # SELECT (ROUND((AVG(`products`.`price`)), 2)) FROM `products`
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(ROUND({sql}, {int(decimals)}))', p, float, c)

    @staticmethod
    def Floor(value):
        """Round a number down to the nearest integer using MySQL's ``FLOOR()``.

        Generates a ``FLOOR(<expr>)`` expression. For positive numbers
        this behaves like truncation toward zero; for negative numbers it
        rounds away from zero (``FLOOR(-1.5) = -2``, unlike
        ``CAST(-1.5 AS SIGNED)`` which gives ``-1``).

        MySQL returns an INTEGER when the input is an INTEGER or DECIMAL;
        the ``current_datatype`` is set to ``int``.

        Args:
            value: The expression to floor. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(FLOOR(<expr>))`` and whose ``current_datatype`` is ``int``.

        Example:
            Snap a price down to the whole dollar::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='store')
                products = db.products

                rows = products.get_row([
                    products.name,
                    products.price,
                    Builtins.Floor(products.price),
                ])
                # SELECT `products`.`name`, `products`.`price`,
                #        (FLOOR(`products`.`price`))
                # FROM `products`
                # -> e.g. [('Widget', 19.99, 19), ('Gadget', 24.50, 24), ...]

            Bucket scores into deciles::

                decile = Builtins.Floor(products.score / 10) * 10
                rows = products.get_row([decile, Builtins.Count('*')])
                # SELECT ((FLOOR((`products`.`score` / %s))) * %s),
                #        (COUNT(*))
                # FROM `products`
                # GROUP BY ... (grouping is done by caller)
                # Parameters: [10, 10]

            Handle negative values correctly (FLOOR rounds down, not
            toward zero)::

                rows = products.get_row([
                    Builtins.Floor(-1.5),   # -> -2.0 in MySQL
                    Builtins.Int(-1.5),     # -> -1 (CAST truncates, not floor)
                ])

            Filter by floored value::

                rows = products.get_row(
                    [products.name, products.price],
                    where=Builtins.Floor(products.price) >= 20,
                )
                # SELECT ... WHERE ((FLOOR(`products`.`price`)) >= %s)
                # Parameters: [20]

            Combined with arithmetic — stays numeric::

                discount = Builtins.Floor(products.price * 0.8)
                rows = products.get_row([products.name, discount])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(FLOOR({sql}))', p, int, c)

    @staticmethod
    def Ceil(value):
        """Round a number up to the nearest integer using MySQL's ``CEIL()``.

        Generates a ``CEIL(<expr>)`` expression. For positive numbers
        this rounds away from zero; for negative numbers it rounds toward
        zero (``CEIL(-1.5) = -1``, unlike ``FLOOR(-1.5) = -2``). This is
        the mirror image of :meth:`Floor`.

        MySQL returns an INTEGER when the input is an INTEGER or DECIMAL;
        the ``current_datatype`` is set to ``int``.

        Args:
            value: The expression to ceil. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CEIL(<expr>))`` and whose ``current_datatype`` is ``int``.

        Example:
            Snap a price up to the whole dollar — a common "don't lose
            money on rounding" pattern::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='store')
                products = db.products

                rows = products.get_row([
                    products.name,
                    products.price,
                    Builtins.Ceil(products.price),
                ])
                # SELECT `products`.`name`, `products`.`price`,
                #        (CEIL(`products`.`price`))
                # FROM `products`
                # -> e.g. [('Widget', 19.01, 20), ('Gadget', 24.00, 24), ...]

            Pagination — compute how many pages of 20 items each::

                page_count = Builtins.Ceil(Builtins.Count('*') / 20)
                rows = products.get_row([page_count])
                # SELECT (CEIL(((COUNT(*)) / %s))) FROM `products`
                # Parameters: [20]

            Bucket scores into the next higher 10::

                next_ten = Builtins.Ceil(products.score / 10) * 10
                rows = products.get_row([products.id, next_ten])

            Handle negatives correctly::

                rows = products.get_row([
                    Builtins.Ceil(-1.5),   # -> -1 in MySQL
                    Builtins.Int(-1.5),    # -> -1 (same here, different reason)
                ])

            Combine with Floor to build a rounded-down/rounded-up pair::

                low  = Builtins.Floor(products.price)
                high = Builtins.Ceil(products.price)
                rows = products.get_row([products.price, low, high])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CEIL({sql}))', p, int, c)

    # =================================================================== type cast

    @staticmethod
    def Int(value):
        """Convert a value to an integer using MySQL's ``CAST(x AS SIGNED)``.

        This is the SQL equivalent of Python's ``int()``. Generates a
        ``CAST(<expr> AS SIGNED)`` expression — MySQL's 64-bit signed
        integer type. MySQL's conversion rules apply: text is parsed as
        a number (leading digits only), REAL is truncated toward zero,
        and NULL stays NULL.

        The result is always an ``INTEGER``, so arithmetic chains and
        comparisons behave as expected even when the original column was
        declared as TEXT or DECIMAL.

        .. note::
            Use ``Builtins.Int`` (SIGNED) rather than a hypothetical
            ``CAST AS INTEGER`` — MySQL rejects ``INTEGER`` in a CAST
            expression; the correct name is ``SIGNED`` (or
            ``UNSIGNED`` for non-negative values).

        Args:
            value: The expression to convert. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CAST(<expr> AS SIGNED))`` and whose ``current_datatype``
            is always ``int``.

        Example:
            Force a text column to be treated as an integer::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='app')
                events = db.events

                rows = events.get_row([
                    events.id,
                    Builtins.Int(events.payload),
                ])
                # SELECT `events`.`id`,
                #        (CAST(`events`.`payload` AS SIGNED))
                # FROM `events`

            Compare a text column numerically::

                rows = events.get_row(
                    [events.id],
                    where=Builtins.Int(events.amount_text) > 1000,
                )
                # SELECT `events`.`id` FROM `events`
                # WHERE ((CAST(`events`.`amount_text` AS SIGNED)) > %s)
                # Parameters: [1000]

            Truncate a REAL toward zero::

                rows = events.get_row([
                    Builtins.Int(events.temperature),   # 23.7 -> 23
                ])
                # SELECT (CAST(`events`.`temperature` AS SIGNED))
                # FROM `events`

            Combine with arithmetic — stays numeric::

                doubled = Builtins.Int(events.count) * 2
                # SELECT ((CAST(`events`.`count` AS SIGNED)) * %s)
                # FROM `events`

            Used inside an aggregate::

                rows = events.get_row([
                    Builtins.Sum(Builtins.Int(events.payload)),
                ])
                # SELECT (SUM((CAST(`events`.`payload` AS SIGNED))))
                # FROM `events`
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CAST(FLOOR({sql}) AS SIGNED))', p, int, c)
    
    @staticmethod
    def Float(value):
        """Convert a value to a floating-point number using ``CAST(x AS DECIMAL)``.

        This is the SQL equivalent of Python's ``float()``. Generates a
        ``CAST(<expr> AS DECIMAL(65,30))`` expression. MySQL uses
        DECIMAL for exact numerics; the ``(65,30)`` precision pair gives
        us a fixed-point type wide enough to hold any IEEE-754 double
        without losing significant digits, effectively emulating a float.

        Use ``Builtins.Func('CAST', value, 'DOUBLE')`` if you specifically
        need the DOUBLE storage class (e.g. for inf/NaN support, though
        MySQL does not support those values in DOUBLE either). For the
        vast majority of numeric conversions DECIMAL is safer and does
        not accumulate floating-point error.

        The result is always a DECIMAL, exposed here as ``float``, so any
        chained arithmetic naturally stays in the floating domain.

        Args:
            value: The expression to convert. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CAST(<expr> AS DECIMAL(65,30)))`` and whose
            ``current_datatype`` is always ``float``.

        Example:
            Force integer division to produce a real result::

                from Ormophine.Mysql import Driver, Builtins

                db         = Driver(host='localhost', port=3306, username='root',
                                    password='secret', db_name='analytics')
                statistics = db.statistics

                ratio = Builtins.Float(statistics.successes) / statistics.attempts
                rows = statistics.get_row([ratio])
                # SELECT ((CAST(`statistics`.`successes` AS DECIMAL(65,30)))
                #         / `statistics`.`attempts`)
                # FROM `statistics`
                # -> e.g. [(0.9732,)]

            Parse a text column as a number::

                rows = statistics.get_row(
                    [statistics.id, Builtins.Float(statistics.amount_text)],
                    where=Builtins.Float(statistics.amount_text) >= 99.5,
                )
                # SELECT `statistics`.`id`,
                #        (CAST(`statistics`.`amount_text` AS DECIMAL(65,30)))
                # FROM `statistics`
                # WHERE ((CAST(`statistics`.`amount_text` AS DECIMAL(65,30))) >= %s)
                # Parameters: [99.5]

            Widen an integer column before averaging::

                rows = statistics.get_row([
                    Builtins.Avg(Builtins.Float(statistics.count)),
                ])

            Multiply a converted value by a literal::

                scaled = Builtins.Float(statistics.score) * 1.5
                rows = statistics.get_row([scaled])
                # SELECT ((CAST(`statistics`.`score` AS DECIMAL(65,30))) * %s)
                # FROM `statistics`
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CAST({sql} AS DECIMAL(65,30)))', p, float, c)

    @staticmethod
    def Str(value):
        """Convert a value to text using MySQL's ``CAST(x AS CHAR)``.

        This is the SQL equivalent of Python's ``str()``. Generates a
        ``CAST(<expr> AS CHAR)`` expression. MySQL's conversion rules
        apply: numbers are formatted as their decimal representation,
        NULL stays NULL, and BLOB data is interpreted as text.

        The result is always a ``TEXT`` value, so chaining with ``+``
        produces string concatenation (``||``) rather than arithmetic
        addition.

        Args:
            value: The expression to convert. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CAST(<expr> AS CHAR))`` and whose ``current_datatype`` is
            always ``str``.

        Example:
            Safely concatenate a numeric column with text::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='company')
                employees = db.employees

                label = Builtins.Str(employees.salary) + ' USD'
                rows = employees.get_row([employees.name, label])
                # SELECT `employees`.`name`,
                #        ((CAST(`employees`.`salary` AS CHAR)) || %s)
                # FROM `employees`
                # Parameters: [' USD']
                #
                # Without Builtins.Str, `employees.salary + ' USD'` would
                # compile to arithmetic '+' (because salary.datatype is int),
                # which MySQL would coerce the string to 0.

            Format an integer id for display::

                rows = employees.get_row([
                    Builtins.Str(employees.id).add_first('EMP-'),
                ])
                # SELECT (%s || (CAST(`employees`.`id` AS CHAR)))
                # FROM `employees`
                # Parameters: ['EMP-']

            Use with string methods::

                rows = employees.get_row([
                    Builtins.Str(employees.salary).strip(),
                ])

            Inside a LIKE pattern::

                rows = employees.get_row(
                    [employees.id],
                    where=Builtins.Str(employees.id).startswith('1'),
                )
                # SELECT `employees`.`id` FROM `employees`
                # WHERE ((CAST(`employees`.`id` AS CHAR)) LIKE %s || '%')
                # Parameters: ['1']
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CAST({sql} AS CHAR))', p, str, c)

    @staticmethod
    def Bool(value):
        """Convert a value to a boolean-like integer using ``CAST(x AS SIGNED)``.

        MySQL has no native boolean type — booleans are stored as
        ``TINYINT(1)`` with the integers ``0`` and ``1``. This helper
        mirrors Python's ``bool()`` by emitting a ``CAST(<expr> AS SIGNED)``
        expression and declaring the result as an ``INTEGER``. The caller
        is responsible for interpreting the returned integer as a truth
        value.

        In practice this is identical to :meth:`Int`; the separate name
        exists so that call sites read as intent (``Builtins.Bool(...)``
        rather than ``Builtins.Int(...)`` when the semantic meaning is a
        truth value).

        Args:
            value: The expression to convert. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``True``, ``False``, ``0``, ``1``,
                  or any coercible value) — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CAST(<expr> AS SIGNED))`` and whose ``current_datatype``
            is ``int``.

        Example:
            Normalise an is_active column that stores mixed values::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                normalised = Builtins.Bool(users.is_active)
                rows = users.get_row([users.id, normalised])
                # SELECT `users`.`id`, (CAST(`users`.`is_active` AS SIGNED))
                # FROM `users`

            Compare a computed condition against a boolean literal::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.Bool(users.age >= 18) == 1,
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE ((CAST((`users`.`age` >= %s) AS SIGNED)) = %s)
                # Parameters: [18, 1]

            Coerce a raw literal — useful when the surrounding API
            expects a ColumnsOperation-shaped object::

                rows = users.get_row([
                    Builtins.Bool(True),    # -> (CAST(%s AS SIGNED)) with param True
                    Builtins.Bool(False),   # -> (CAST(%s AS SIGNED)) with param False
                ])

            Use as a numeric value in a SUM::

                active_count = Builtins.Sum(Builtins.Bool(users.is_active))
                rows = users.get_row([active_count])
                # SELECT (SUM((CAST(`users`.`is_active` AS SIGNED))))
                # FROM `users`
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CAST({sql} AS SIGNED))', p, int, c)

    @staticmethod
    def Sign(value):
        """Return the sign of a number using MySQL's ``SIGN()`` function.

        Generates a ``SIGN(<expr>)`` expression. MySQL returns:

        - ``-1`` if the value is negative
        - `` 0`` if the value is zero
        - `` 1`` if the value is positive

        This mirrors Python's ``(x > 0) - (x < 0)`` idiom and is useful
        for bucketing, sorting by direction, or normalising deltas.

        Args:
            value: The expression whose sign is computed. Supported
                types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(SIGN(<expr>))`` and whose ``current_datatype`` is ``int``.

        Example:
            Bucket movements by direction — up, flat, or down::

                from Ormophine.Mysql import Driver, Builtins

                db        = Driver(host='localhost', port=3306, username='root',
                                   password='secret', db_name='analytics')
                movements = db.movements

                rows = movements.get_row([
                    movements.account_id,
                    movements.delta,
                    Builtins.Sign(movements.delta),
                ])
                # SELECT `movements`.`account_id`, `movements`.`delta`,
                #        (SIGN(`movements`.`delta`))
                # FROM `movements`
                # -> e.g. [(1, 250.0, 1), (1, -80.0, -1), (1, 0.0, 0), ...]

            Filter only the positive movements::

                rows = movements.get_row(
                    [movements.account_id, movements.delta],
                    where=Builtins.Sign(movements.delta) == 1,
                )
                # SELECT ... WHERE ((SIGN(`movements`.`delta`)) = %s)
                # Parameters: [1]

            Sum signs to count net direction — positive means more ups
            than downs, and vice versa::

                net = Builtins.Sum(Builtins.Sign(movements.delta))
                rows = movements.get_row([movements.account_id, net])
                # SELECT `movements`.`account_id`,
                #        (SUM((SIGN(`movements`.`delta`))))
                # FROM `movements`
                # GROUP BY ... (grouping is done by caller)

            Sign of a computed expression::

                range_mid = (movements.high + movements.low) / 2
                rows = movements.get_row([
                    movements.symbol,
                    Builtins.Sign(movements.close - range_mid),
                ])
                # SELECT `movements`.`symbol`,
                #        (SIGN((`movements`.`close` - ((`movements`.`high` + `movements`.`low`) / %s))))
                # FROM `movements`
                # Parameters: [2]

            Chained with arithmetic::

                direction_times_size = Builtins.Sign(movements.delta) * movements.delta
                rows = movements.get_row([
                    movements.account_id,
                    direction_times_size,
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(SIGN({sql}))', p, int, c)

    @staticmethod
    def Sqrt(value):
        """Compute the square root of a number using MySQL's ``SQRT()`` function.

        Generates a ``SQRT(<expr>)`` expression. The result is always a
        REAL (float) — even ``SQRT(4)`` returns ``2.0``, not ``2``.

        Negative inputs produce ``NULL`` in MySQL rather than raising an
        error. If your application needs to distinguish "no result" from
        "zero", wrap the call or add a ``CHECK`` constraint on the column.

        Args:
            value: The expression whose square root is computed. Supported
                types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value (``int``, ``float``) — bound as a
                  ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(SQRT(<expr>))`` and whose ``current_datatype`` is always
            ``float``.

        Example:
            Euclidean distance — the standard ``sqrt(a² + b²)`` pattern::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='geometry')
                points = db.points

                distance_from_origin = Builtins.Sqrt(
                    points.x * points.x + points.y * points.y
                )
                rows = points.get_row([
                    points.id,
                    distance_from_origin,
                ])
                # SELECT `points`.`id`,
                #        (SQRT(((`points`.`x` * `points`.`x`)
                #                + (`points`.`y` * `points`.`y`))))
                # FROM `points`

            Standard deviation of a small set — square root of the mean
            squared deviation::

                mean = Builtins.Avg(points.x)
                sq_dev = Builtins.Avg((points.x - mean) * (points.x - mean))
                stddev = Builtins.Sqrt(sq_dev)
                rows = points.get_row([stddev])
                # SELECT (SQRT((AVG(((`points`.`x` - (AVG(`points`.`x`)))
                #                     * (`points`.`x` - (AVG(`points`.`x`))))))))
                # FROM `points`

            Filter by magnitude — points more than 10 units from origin::

                rows = points.get_row(
                    [points.id, points.x, points.y],
                    where=Builtins.Sqrt(
                        points.x * points.x + points.y * points.y
                    ) > 10,
                )

            Combine with Round for display::

                pretty = Builtins.Round(
                    Builtins.Sqrt(points.x * points.x + points.y * points.y) * 100
                ) / 100
                rows = points.get_row([points.id, pretty])

            Applied to a raw literal::

                rows = points.get_row([
                    Builtins.Sqrt(16),    # -> (SQRT(%s)) with param 16 -> 4.0
                    Builtins.Sqrt(2),     # -> 1.41421356...
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(SQRT({sql}))', p, float, c)

    @staticmethod
    def Pow(base, exp):
        """Raise a base value to a power using MySQL's ``POW(a, b)`` function.

        Generates a ``POW(<base>, <exp>)`` expression. This is the SQL
        analogue of Python's built-in ``pow(base, exp)`` and the ``**``
        operator. Both operands may be any expression — a column, another
        builtin, a raw literal, or a nested :class:`ColumnsOperation`.

        The result is always a REAL (float) — even ``POW(2, 3)`` returns
        ``8.0``, not ``8``, because MySQL keeps all math functions in the
        floating-point domain (returning ``DOUBLE``).

        Args:
            base: The base expression. Any of the standard operand types:

                - :class:`ColumnsOperation`
                - :class:`Column`
                - A raw Python number

            exp: The exponent expression. Same accepted types as ``base``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(POW(<base>, <exp>))`` and whose ``current_datatype`` is
            always ``float``.

        Example:
            Compound interest — principal times ``(1 + rate)^years``::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='bank')
                accounts = db.accounts

                future_value = accounts.principal * Builtins.Pow(
                    1 + accounts.rate,
                    accounts.years,
                )
                rows = accounts.get_row([
                    accounts.id,
                    future_value,
                ])
                # SELECT `accounts`.`id`,
                #        (`accounts`.`principal`
                #         * (POW((%s + `accounts`.`rate`), `accounts`.`years`)))
                # FROM `accounts`
                # Parameters: [1]

            Square or cube a value::

                rows = accounts.get_row([
                    Builtins.Pow(accounts.principal, 2),   # square
                    Builtins.Pow(accounts.principal, 3),   # cube
                ])
                # SELECT (POW(`accounts`.`principal`, %s)),
                #        (POW(`accounts`.`principal`, %s))
                # FROM `accounts`

            Square root via fractional exponent — equivalent to
            :meth:`Sqrt`::

                rows = accounts.get_row([
                    Builtins.Pow(accounts.principal, 0.5),
                ])
                # SELECT (POW(`accounts`.`principal`, %s)) FROM `accounts`
                # Parameters: [0.5]

            Inverse power — ``x⁻¹`` equals ``1/x``::

                rows = accounts.get_row([
                    Builtins.Pow(accounts.principal, -1),
                ])

            Distance squared without the square root::

                rows = accounts.get_row(
                    [accounts.id],
                    where=Builtins.Pow(accounts.x, 2)
                        + Builtins.Pow(accounts.y, 2) > 100,
                )

            Combine with Round for display::

                pretty = Builtins.Round(
                    Builtins.Pow(accounts.principal, 1.05) * 100
                ) / 100
                rows = accounts.get_row([accounts.id, pretty])

            Chained with arithmetic — stays numeric::

                doubled = Builtins.Pow(accounts.principal, 2) * 2
                # SELECT ((POW(`accounts`.`principal`, %s)) * %s)
                # FROM `accounts`
        """
        s1, p1, _, c = Builtins._normalize(base)
        s2, p2, _, _ = Builtins._normalize(exp)
        return Builtins._make(f'(POW({s1}, {s2}))', p1 + p2, float, c)

    # ================================================================ condition

    @staticmethod
    def IsNull(value):
        """Test whether a value is NULL.

        Generates an ``(<expr> IS NULL)`` predicate. MySQL evaluates this
        to ``1`` (true) when the expression evaluates to NULL, and ``0``
        (false) otherwise.

        This is the explicit, always-correct way to check for NULL. Using
        the ordinary comparison operator — ``column == None`` — is also
        supported by this ORM (see :meth:`Column.eq`) but only when the
        right-hand side is literally ``None``. ``IsNull`` works uniformly
        with any expression:

        - A column that may contain NULL
        - A computed expression that may return NULL (e.g. ``Sqrt`` of a
          negative number, ``Find`` returning no match, aggregate over an
          empty group with ``Sum``)
        - A raw Python ``None`` literal

        Args:
            value: The expression to test. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder. In
                  particular, ``IsNull(None)`` produces ``(%s IS NULL)``
                  with the ``None`` bound as a parameter, which is always
                  true.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``((<expr>) IS NULL)`` and whose ``current_datatype`` is
            ``int`` — MySQL returns 0 or 1 for boolean predicates.

        Example:
            Select users who have not yet provided an email::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.username],
                    where=Builtins.IsNull(users.email),
                )
                # SELECT `users`.`id`, `users`.`username` FROM `users`
                # WHERE ((`users`.`email`) IS NULL)

            Negate the check — see :meth:`IsNotNull`::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.IsNotNull(users.email),
                )

            Combine with AND to build richer filters::

                rows = users.get_row(
                    [users.id, users.username],
                    where=Builtins.IsNull(users.email)
                        & (users.created_at > '2024-01-01'),
                )
                # SELECT ... WHERE (((`users`.`email`) IS NULL)
                #                    AND (`users`.`created_at` > %s))
                # Parameters: ['2024-01-01']

            Test a computed expression that might return NULL::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.IsNull(
                        Builtins.Sqrt(users.balance - users.debt)
                    ),
                )
                # -> users whose balance < debt (SQRT returns NULL)
                # SELECT `users`.`id` FROM `users`
                # WHERE ((SQRT((`users`.`balance` - `users`.`debt`))) IS NULL)

            Count how many rows have a NULL value in one column::

                missing_emails = Builtins.Sum(Builtins.IsNull(users.email))
                rows = users.get_row([missing_emails])
                # SELECT (SUM(((`users`.`email`) IS NULL))) FROM `users`
                # -> [(42,)] if 42 users have no email

            Check for a raw literal ``None``::

                rows = users.get_row([
                    Builtins.IsNull(None),   # -> ((%s) IS NULL) -> always 1
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(({sql}) IS NULL)', p, int, c)

    @staticmethod
    def IsNotNull(value):
        """Test whether a value is not NULL.

        Generates an ``(<expr> IS NOT NULL)`` predicate. MySQL evaluates
        this to ``1`` (true) when the expression produces any non-NULL
        value, and ``0`` (false) when it evaluates to NULL.

        The exact logical negation of :meth:`IsNull`. Use whichever reads
        more naturally at the call site — both compile to distinct SQL
        but produce the same set of rows when their results are inverted.

        Args:
            value: The expression to test. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder. In
                  particular, ``IsNotNull(42)`` produces
                  ``(%s IS NOT NULL)`` with the ``42`` bound as a
                  parameter, which is always true; ``IsNotNull(None)`` is
                  always false.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``((<expr>) IS NOT NULL)`` and whose ``current_datatype`` is
            ``int``.

        Example:
            Select users who have provided an email::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.username, users.email],
                    where=Builtins.IsNotNull(users.email),
                )
                # SELECT `users`.`id`, `users`.`username`, `users`.`email`
                # FROM `users`
                # WHERE ((`users`.`email`) IS NOT NULL)

            Combine with OR to allow either of two identifier columns::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.IsNotNull(users.email)
                        | Builtins.IsNotNull(users.phone),
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE (((`users`.`email`) IS NOT NULL)
                #        OR ((`users`.`phone`) IS NOT NULL))

            Count non-NULL values in a column — the inverse of the
            ``IsNull`` count::

                have_email = Builtins.Sum(Builtins.IsNotNull(users.email))
                rows = users.get_row([have_email])
                # SELECT (SUM(((`users`.`email`) IS NOT NULL))) FROM `users`

            Test a computed expression whose non-NULLness is meaningful::

                valid_ratio = Builtins.Floor(
                    users.successes / users.attempts
                )
                rows = users.get_row(
                    [users.id],
                    where=Builtins.IsNotNull(valid_ratio),
                )
                # -> users whose attempts > 0 (division by zero yields NULL)

            Chained with a case check — the classic "both or neither"
            validation pattern::

                rows = users.get_row(
                    [users.id],
                    where=(Builtins.IsNotNull(users.email)
                            & Builtins.IsNotNull(users.verified_at))
                        | (Builtins.IsNull(users.email)
                            & Builtins.IsNull(users.verified_at)),
                )
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(({sql}) IS NOT NULL)', p, int, c)

    @staticmethod
    def Between(value, low, high):
        """Test whether a value lies within an inclusive range.

        Generates an ``(<expr> BETWEEN <low> AND <high>)`` predicate.
        MySQL evaluates this to ``1`` (true) when ``low <= value <= high``
        and ``0`` otherwise. Both bounds are inclusive, matching Python's
        ``low <= x <= high`` idiom exactly.

        .. note::
            MySQL's ``BETWEEN`` is equivalent to
            ``(value >= low) AND (value <= high)`` but is a single
            operator with well-defined semantics for all three operand
            types. When any of the three operands is NULL the result is
            NULL, which the surrounding query treats as "false" in a
            ``WHERE`` clause.

        Args:
            value: The expression to test. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python value — bound as a ``%s`` placeholder.

            low: The lower bound (inclusive). Same accepted types as
                ``value``.

            high: The upper bound (inclusive). Same accepted types as
                ``value``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``((<value>) BETWEEN <low> AND <high>)`` and whose
            ``current_datatype`` is ``int``. Parameters are concatenated
            left-to-right: first ``value``'s, then ``low``'s, then
            ``high``'s.

        Example:
            Select products in a price band::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='store')
                products = db.products

                rows = products.get_row(
                    [products.name, products.price],
                    where=Builtins.Between(products.price, 10.0, 50.0),
                )
                # SELECT `products`.`name`, `products`.`price` FROM `products`
                # WHERE ((`products`.`price`) BETWEEN %s AND %s)
                # Parameters: [10.0, 50.0]

            Date-range filter — the most common real-world use::

                users = db.users
                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Between(
                        users.created_at,
                        '2024-01-01',
                        '2024-12-31',
                    ),
                )
                # SELECT ... WHERE ((`users`.`created_at`)
                #                    BETWEEN %s AND %s)
                # Parameters: ['2024-01-01', '2024-12-31']

            Filter on a computed expression::

                rows = products.get_row(
                    [products.name],
                    where=Builtins.Between(
                        products.price * 0.9,      # discounted price
                        5.0,
                        45.0,
                    ),
                )
                # SELECT `products`.`name` FROM `products`
                # WHERE ((`products`.`price` * %s) BETWEEN %s AND %s)
                # Parameters: [0.9, 5.0, 45.0]

            Boundaries using other columns::

                rows = products.get_row(
                    [products.name, products.price],
                    where=Builtins.Between(
                        products.price,
                        products.cost,
                        products.cost * 2,
                    ),
                )
                # SELECT ... WHERE ((`products`.`price`)
                #                    BETWEEN `products`.`cost`
                #                        AND (`products`.`cost` * %s))
                # Parameters: [2]

            Negate the check — rows outside the band::

                in_band = Builtins.Between(products.price, 10.0, 50.0)
                rows = products.get_row(
                    [products.name],
                    where=(in_band == 0),
                )
                # SELECT `products`.`name` FROM `products`
                # WHERE ((((`products`.`price`) BETWEEN %s AND %s)) = %s)
                # Parameters: [10.0, 50.0, 0]

            NULL handling — a NULL value in any operand makes the whole
            predicate NULL (treated as false). To include NULLs, add an
            explicit OR::

                rows = products.get_row(
                    [products.name],
                    where=Builtins.Between(products.price, 10.0, 50.0)
                        | Builtins.IsNull(products.price),
                )
        """
        s,  p,  _,  c  = Builtins._normalize(value)
        lo, lp, _,  _  = Builtins._normalize(low)
        hi, hp, _,  _  = Builtins._normalize(high)
        return Builtins._make(f'(({s}) BETWEEN {lo} AND {hi})', p + lp + hp, int, c)

    @staticmethod
    def IIf(condition, then_value, else_value):
        """One-line conditional — the SQL analogue of Python's ``a if cond else b``.

        Generates an ``IF(<cond>, <then>, <else>)`` expression. MySQL
        evaluates ``condition`` first; if it is truthy (any non-zero,
        non-NULL value), the expression yields ``then_value``; otherwise
        it yields ``else_value``.

        This is the direct equivalent of Python's ternary conditional
        operator::

            Python:   x if cond else y
            SQL:      IF(cond, x, y)
            ORM:      Builtins.IIf(cond, x, y)

        .. note::
            Unlike Python's ``and`` / ``or``, MySQL's ``IF`` **always
            evaluates both branches** before choosing one. This matters
            for expressions with potential errors — e.g. division by
            zero returns NULL rather than skipping. If both branches
            might divide by zero, guard the divisor with an inner
            ``IIf``.

        Args:
            condition: The test expression. May be any standard operand:

                - :class:`ColumnsOperation` — typically a comparison
                  like ``users.age >= 18``.
                - :class:`Column` — used directly as a boolean.
                - A raw Python value (``True``, ``False``, ``0``, ``1``)
                  — bound as a parameter. MySQL treats ``0`` as false
                  and any other number as true.

            then_value: What to return when ``condition`` is truthy.
                Same accepted types as ``condition``.

            else_value: What to return when ``condition`` is falsy.
                Same accepted types as ``condition``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(IF(<cond>, <then>, <else>))`` and whose
            ``current_datatype`` is ``None``. The datatype is not
            propagated because the two branches may disagree (e.g.
            ``str`` versus ``int``); downstream ``+`` operators will
            choose arithmetic by default, which the caller may override
            by wrapping the result in :meth:`Str`, :meth:`Int`, or
            :meth:`Float`.

        Example:
            Categorise ages into minor / adult — the classic pattern::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                category = Builtins.IIf(users.age >= 18, 'adult', 'minor')
                rows = users.get_row([
                    users.name,
                    users.age,
                    category,
                ])
                # SELECT `users`.`name`, `users`.`age`,
                #        (IF((`users`.`age` >= %s), %s, %s))
                # FROM `users`
                # Parameters: [18, 'adult', 'minor']
                # -> [('Alice', 30, 'adult'), ('Bob', 15, 'minor'), ...]

            Fill NULL with a fallback — equivalent to ``COALESCE`` but
            with a boolean test::

                display_email = Builtins.IIf(
                    Builtins.IsNull(users.email),
                    'no email',
                    users.email,
                )
                rows = users.get_row([users.username, display_email])
                # SELECT `users`.`username`,
                #        (IF(((`users`.`email`) IS NULL), %s, `users`.`email`))
                # FROM `users`
                # Parameters: ['no email']

            Nested conditionals — a two-threshold bucketing::

                label = Builtins.IIf(
                    users.score >= 90,
                    'A',
                    Builtins.IIf(
                        users.score >= 80,
                        'B',
                        Builtins.IIf(
                            users.score >= 70,
                            'C',
                            'F',
                        ),
                    ),
                )
                rows = users.get_row([users.name, users.score, label])
                # The nesting produces deeply parenthesised SQL:
                # (IF((`users`.`score` >= %s), ?, (IF((`users`.`score` >= %s),
                #      ?, (IF((`users`.`score` >= %s), ?, ?))))))
                # Parameters: [90, 'A', 80, 'B', 70, 'C', 'F']

            Conditional arithmetic — bonus only for top performers::

                adjusted_salary = Builtins.IIf(
                    users.rating > 4,
                    users.salary * 1.1,
                    users.salary,
                )
                rows = users.get_row([
                    users.name,
                    users.salary,
                    adjusted_salary,
                ])
                # SELECT `users`.`name`, `users`.`salary`,
                #        (IF((`users`.`rating` > %s),
                #             (`users`.`salary` * %s),
                #             `users`.`salary`))
                # FROM `users`
                # Parameters: [4, 1.1]

            Coerce the result to a specific type with a cast builtin::

                label_num = Builtins.Int(
                    Builtins.IIf(users.is_active == 1, 100, 0)
                )
                rows = users.get_row([label_num])

            Count matches via SUM of IF — a common "conditional
            aggregate" idiom::

                active_count = Builtins.Sum(
                    Builtins.IIf(users.is_active == 1, 1, 0)
                )
                rows = users.get_row([active_count])
                # SELECT (SUM((IF((`users`.`is_active` = %s), %s, %s))))
                # FROM `users`
                # Parameters: [1, 1, 0]

            Diverging branches with different datatypes — the reason
            ``current_datatype`` is ``None`` on the result::

                mixed = Builtins.IIf(users.has_phone == 1, users.phone, 0)
                # If has_phone, returns a string; else returns integer 0.
                # Wrap with Builtins.Str if the surrounding context expects
                # a string:
                safe = Builtins.Str(mixed)
        """
        cs, cp, _, c = Builtins._normalize(condition)
        ts, tp, _, _ = Builtins._normalize(then_value)
        es, ep, _, _ = Builtins._normalize(else_value)
        return Builtins._make(f'(IF({cs}, {ts}, {es}))', cp + tp + ep, None, c)

    # ============================================================ date and time

    @staticmethod
    def Date(value):
        """Extract the date portion of a value using MySQL's ``DATE()`` function.

        Generates a ``DATE(<expr>)`` expression. The result is a TEXT
        string in ``'YYYY-MM-DD'`` format. Inputs accepted by MySQL
        include:

        - An ISO date/datetime string: ``'2024-03-15'``,
          ``'2024-03-15 09:30:00'``
        - A ``DATE``/``DATETIME``/``TIMESTAMP`` column
        - A numeric Unix epoch value (converted implicitly)
        - The literal string ``'now'`` — special-cased here to emit
          MySQL's ``CURDATE()``

        This is the SQL equivalent of Python's ``datetime.date()``
        method: it discards the time-of-day component.

        Args:
            value: The expression whose date component is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.
                - The literal string ``'now'`` — special-cased to
                  ``CURDATE()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DATE(<expr>))`` (or ``(CURDATE())`` for the ``'now'``
            case) and whose ``current_datatype`` is ``str``.

        Example:
            Get the signup date for every user — strip the time
            component::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([
                    users.id,
                    users.created_at,
                    Builtins.Date(users.created_at),
                ])
                # SELECT `users`.`id`, `users`.`created_at`,
                #        (DATE(`users`.`created_at`))
                # FROM `users`
                # -> [(1, '2024-03-15 09:30:42', '2024-03-15'), ...]

            Filter by a specific day — everything created on March 15::

                rows = users.get_row(
                    [users.id, users.username],
                    where=Builtins.Date(users.created_at) == '2024-03-15',
                )
                # SELECT `users`.`id`, `users`.`username` FROM `users`
                # WHERE ((DATE(`users`.`created_at`)) = %s)
                # Parameters: ['2024-03-15']

            Group by day and count — do the grouping in Python after
            fetching, since the ORM has no GROUP BY helper::

                rows = users.get_row([Builtins.Date(users.created_at)])
                from collections import Counter
                daily = Counter(d for (d,) in rows)
                # {'2024-03-15': 12, '2024-03-16': 8, ...}

            Compare against the current date — the ``'now'`` keyword
            special case::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.Date(users.created_at) == Builtins.Date('now'),
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE ((DATE(`users`.`created_at`)) = (CURDATE()))

            Chain with date modifiers for windows of time::

                first_of_month = Builtins.DateAdd(users.created_at, 'start of month')
                rows = users.get_row([
                    users.id,
                    first_of_month,
                ])
                # SELECT `users`.`id`,
                #        (DATE(DATE_FORMAT(`users`.`created_at`, '%Y-%m-01')))
                # FROM `users`

            Pass a raw literal::

                rows = users.get_row([
                    Builtins.Date('2024-03-15 14:30:00'),   # -> '2024-03-15'
                ])
        """
        if Builtins._is_now(value):
            return Builtins._make('(CURDATE())', [], str, Builtins._NullCol)
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(DATE({sql}))', p, str, c)

    @staticmethod
    def Time(value):
        """Extract the time-of-day portion of a value using MySQL's ``TIME()``.

        Generates a ``TIME(<expr>)`` expression. The result is a TEXT
        string in ``'HH:MM:SS'`` format. Inputs accepted by MySQL mirror
        those of :meth:`Date`:

        - An ISO date/datetime string: ``'2024-03-15 09:30:00'``
        - A ``TIME``/``DATETIME``/``TIMESTAMP`` column
        - The literal string ``'now'`` — special-cased here to emit
          MySQL's ``CURTIME()``

        This is the SQL equivalent of Python's ``datetime.time()``
        method: it discards the date component.

        Args:
            value: The expression whose time component is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.
                - The literal string ``'now'`` — special-cased to
                  ``CURTIME()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(TIME(<expr>))`` (or ``(CURTIME())`` for the ``'now'``
            case) and whose ``current_datatype`` is ``str``.

        Example:
            Get the signup time for every user::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([
                    users.id,
                    users.created_at,
                    Builtins.Time(users.created_at),
                ])
                # SELECT `users`.`id`, `users`.`created_at`,
                #        (TIME(`users`.`created_at`))
                # FROM `users`
                # -> [(1, '2024-03-15 09:30:42', '09:30:42'), ...]

            Filter by time-of-day range — office-hours signups::

                rows = users.get_row(
                    [users.id, users.username],
                    where=Builtins.Time(users.created_at) >= '09:00:00'
                        and Builtins.Time(users.created_at) < '17:00:00',
                )
                # Because BETWEEN is inclusive on both ends, use two
                # comparisons for a half-open range.
                # SELECT ... WHERE ((TIME(`users`.`created_at`)) >= %s)
                #               AND ((TIME(`users`.`created_at`)) < %s)
                # Parameters: ['09:00:00', '17:00:00']

            Extract the hour via :meth:`Hour` instead of slicing::

                rows = users.get_row([
                    users.id,
                    Builtins.Hour(users.created_at),
                ])

            Compare against the current time::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.Time(users.created_at)
                        < Builtins.Time('now'),
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE ((TIME(`users`.`created_at`)) < (CURTIME()))

            Pass a raw literal::

                rows = users.get_row([
                    Builtins.Time('2024-03-15 14:30:00'),   # -> '14:30:00'
                ])

            Build a compact "time ago" bucket with IIf::

                bucket = Builtins.IIf(
                    Builtins.Hour(users.created_at) < 12,
                    'AM',
                    'PM',
                )
                rows = users.get_row([users.id, bucket])
        """
        if Builtins._is_now(value):
            return Builtins._make('(CURTIME())', [], str, Builtins._NullCol)
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(TIME({sql}))', p, str, c)

    @staticmethod
    def DateTime(value):
        """Normalise a value to a full datetime using ``CAST(x AS DATETIME)``.

        MySQL has no ``DATETIME()`` normalizer function like SQLite's
        ``datetime()``; the closest equivalent is a ``CAST`` to the
        ``DATETIME`` storage type. The result is a TEXT string in
        ``'YYYY-MM-DD HH:MM:SS'`` format. Inputs accepted by MySQL
        mirror those of :meth:`Date` and :meth:`Time`:

        - An ISO date or datetime string
        - A numeric Unix epoch value (converted implicitly)
        - The literal string ``'now'`` — special-cased here to emit
          MySQL's ``NOW()``

        Its main uses are:

        - **Normalising** inputs of mixed shape so they all compare
          consistently.
        - **Upgrading** a date-only string (``'2024-03-15'``) to a full
          datetime (``'2024-03-15 00:00:00'``).
        - **Getting the current timestamp** via ``DateTime('now')``.

        Args:
            value: The expression whose full datetime is produced.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.
                - The literal string ``'now'`` — special-cased to
                  ``NOW()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CAST(<expr> AS DATETIME))`` (or ``(NOW())`` for the
            ``'now'`` case) and whose ``current_datatype`` is ``str``.

        Example:
            Normalise mixed-format timestamps for display::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='app')
                events = db.events

                rows = events.get_row([
                    events.id,
                    events.occurred_at,                       # mixed shapes
                    Builtins.DateTime(events.occurred_at),   # all normalised
                ])
                # SELECT `events`.`id`, `events`.`occurred_at`,
                #        (CAST(`events`.`occurred_at` AS DATETIME))
                # FROM `events`
                # -> [(1, '2024-03-15', '2024-03-15 00:00:00'), ...]

            Get the current datetime as a SELECT column::

                rows = events.get_row([
                    Builtins.DateTime('now'),
                ])
                # SELECT (NOW()) FROM `events`
                # -> [('2024-03-15 14:30:42',)]

            Compare against the current datetime for "last 24 hours"::

                rows = events.get_row(
                    [events.id, events.occurred_at],
                    where=Builtins.DateTime(events.occurred_at)
                        >= Builtins.DateTimeAdd('now', '-1 day'),
                )
                # SELECT `events`.`id`, `events`.`occurred_at` FROM `events`
                # WHERE ((CAST(`events`.`occurred_at` AS DATETIME))
                #         >= (DATE_SUB(NOW(), INTERVAL 1 DAY)))

            Compare against the current datetime for "last week"::

                rows = events.get_row(
                    [events.id],
                    where=Builtins.DateTime(events.occurred_at)
                        >= Builtins.DateTimeAdd('now', '-7 days'),
                )

            Combine with IIf to bucket events into "recent / older"::

                recent = Builtins.IIf(
                    Builtins.DateTime(events.occurred_at)
                        >= Builtins.DateTimeAdd('now', '-1 day'),
                    'recent',
                    'older',
                )
                rows = events.get_row([events.id, recent])

            Store into a datetime column during an UPDATE::

                events.update(
                    update={events.last_seen: Builtins.DateTime('now')},
                    where=events.id == 42,
                )
                # UPDATE `events` SET `last_seen`=(NOW())
                # WHERE (`events`.`id` = %s)

            Pass a raw literal to normalise at the SQL layer::

                rows = events.get_row([
                    Builtins.DateTime('2024-03-15'),           # -> '2024-03-15 00:00:00'
                    Builtins.DateTime('2024-03-15 09:30:00'),  # unchanged
                ])
        """
        if Builtins._is_now(value):
            return Builtins._make('(NOW())', [], str, Builtins._NullCol)
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(CAST({sql} AS DATETIME))', p, str, c)

    @staticmethod
    def Now():
        """Return the current moment as a MySQL ``NOW()`` value.

        Generates a ``NOW()`` expression. The result is a TEXT string in
        ISO 8601 format: ``'YYYY-MM-DD HH:MM:SS'``, in the server's
        session time zone (``@@session.time_zone``), which defaults to
        ``SYSTEM`` (the OS time zone).

        No parameters are consumed. The optional positional argument
        ``_`` exists only to make the signature uniform with the other
        date/time helpers and to allow the callable to be passed to APIs
        that expect a one-argument function. It is ignored.

        For the corresponding date-only form, see :meth:`Today`. For an
        integer Unix timestamp, see :meth:`UnixNow`. For UTC-normalised
        output when the server runs in a non-UTC time zone, chain
        through ``Builtins.Func('CONVERT_TZ', value, '+00:00', ...)`` or
        set ``SET time_zone = '+00:00'`` on the connection.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(NOW())`` and whose ``current_datatype`` is ``str``. The
            parameter list is always empty.

        Example:
            The current timestamp as a SELECT column::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([Builtins.Now()])
                # SELECT (NOW()) FROM `users`
                # -> [('2024-03-15 14:30:42',)] — same value for every row
                #    in the result set, because MySQL evaluates NOW() once
                #    per query, not per row.

            Compute account age in days::

                age = Builtins.DateDiffDays(Builtins.Now(), users.created_at)
                rows = users.get_row([users.username, age])
                # SELECT `users`.`username`, (DATEDIFF(NOW(), `users`.`created_at`))
                # FROM `users`

            Compute account age in a human-readable format::

                age_str = Builtins.Timediff(Builtins.Now(), users.created_at)
                rows = users.get_row([users.username, age_str])
                # -> [('Alice', '02 05:29:18'), ...]

            Filter rows updated in the last hour::

                rows = users.get_row(
                    [users.username, users.updated_at],
                    where=Builtins.DateDiffSeconds(
                        Builtins.Now(), users.updated_at
                    ) < 3600,
                )

            Store the current timestamp during an UPDATE::

                users.update(
                    update={users.last_seen: Builtins.Now()},
                    where=users.id == 42,
                )
                # UPDATE `users` SET `last_seen`=(NOW())
                # WHERE (`users`.`id` = %s);
                # Parameters: [42]

            Insert with an explicit timestamp — use the raw form for
            inserts when the value must be produced by SQL::

                users.custom_execute(
                    "INSERT INTO `users` (`username`, `created_at`) "
                    "VALUES (%s, NOW())",
                    ['alice'],
                )

            Compare two Now() calls to see that both evaluate to the
            same moment in a single query::

                rows = users.get_row([
                    Builtins.Now(),
                    Builtins.Now(),
                ])
                # -> [('2024-03-15 14:30:42', '2024-03-15 14:30:42')]
                # Both columns are identical.

            Force a specific time zone for the current moment::

                utc_now = Builtins.Func('CONVERT_TZ', Builtins.Now(), '+00:00', '+00:00')
                rows = users.get_row([utc_now])
                # -> [('2024-03-15 14:30:42',)] — no-op if server is UTC
        """
        return Builtins._make('(NOW())', [], str, Builtins._NullCol)

    @staticmethod
    def Today(_=None):
        """Return the current date as a MySQL ``CURDATE()`` value.

        Generates a ``CURDATE()`` expression. The result is a TEXT string
        in ISO 8601 format: ``'YYYY-MM-DD'``, in the server's session
        time zone. No parameters are consumed. This is the date-only
        counterpart of :meth:`Now`.

        The optional positional argument ``_`` exists only to make the
        signature uniform with the other date/time helpers; it is
        ignored.

        For the corresponding datetime, see :meth:`Now`. For an integer
        Unix timestamp, see :meth:`UnixNow`.

        Args:
            _: Ignored. Any value passed is discarded; the SQL emitted is
                always ``CURDATE()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(CURDATE())`` and whose ``current_datatype`` is ``str``.
            The parameter list is always empty.

        Example:
            The current date as a SELECT column::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([Builtins.Today()])
                # SELECT (CURDATE()) FROM `users`
                # -> [('2024-03-15',)]

            Find users who signed up today::

                rows = users.get_row(
                    [users.username, users.created_at],
                    where=Builtins.Date(users.created_at) == Builtins.Today(),
                )
                # SELECT `users`.`username`, `users`.`created_at` FROM `users`
                # WHERE ((DATE(`users`.`created_at`)) = (CURDATE()))

            Same-day boolean flag via IIf::

                is_today = Builtins.IIf(
                    Builtins.Date(users.created_at) == Builtins.Today(),
                    'new',
                    'old',
                )
                rows = users.get_row([users.username, is_today])

            Store today's date in an UPDATE::

                users.update(
                    update={users.last_active_date: Builtins.Today()},
                    where=users.id == 42,
                )
                # UPDATE `users` SET `last_active_date`=(CURDATE())
                # WHERE (`users`.`id` = %s);
                # Parameters: [42]

            Yesterday's date — chain with a modifier::

                yesterday = Builtins.DateAdd('now', '-1 day')
                rows = users.get_row([yesterday])
                # -> [('2024-03-14',)]

            First day of the current month — useful for month-to-date
            reporting::

                month_start = Builtins.DateAdd('now', 'start of month')
                rows = users.get_row([month_start])
                # -> [('2024-03-01',)]

            Count users who signed up today::

                today_count = Builtins.Sum(
                    Builtins.IIf(
                        Builtins.Date(users.created_at) == Builtins.Today(),
                        1,
                        0,
                    )
                )
                rows = users.get_row([today_count])
                # -> [(8,)] if 8 users signed up today

            Compare against a fixed date literal::

                rows = users.get_row(
                    [users.username],
                    where=Builtins.Today() > '2024-01-01',
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE ((CURDATE()) > %s)
                # Parameters: ['2024-01-01']
        """
        return Builtins._make('(CURDATE())', [], str, Builtins._NullCol)

    @staticmethod
    def UnixNow(_=None):
        """Return the current moment as an integer Unix timestamp.

        Generates a ``UNIX_TIMESTAMP()`` expression. The result is an
        INTEGER equal to the number of whole seconds since the Unix
        epoch (midnight UTC on 1 January 1970). No parameters are
        consumed.

        This is the numeric counterpart of :meth:`Now` and matches the
        return value of Python's ``time.time()`` (rounded down to an
        integer). Because the result is a plain integer, it is the best
        choice for cache keys, request IDs, expiry timestamps, and any
        arithmetic that needs sub-day precision without floating-point
        round-off.

        Note that MySQL's ``UNIX_TIMESTAMP()`` is time-zone independent
        for the current moment — it always returns the count of seconds
        since the epoch, regardless of ``@@session.time_zone``.

        The optional positional argument ``_`` exists only to make the
        signature uniform with the other date/time helpers; it is
        ignored.

        Args:
            _: Ignored. Any value passed is discarded; the SQL emitted is
                always ``UNIX_TIMESTAMP()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(UNIX_TIMESTAMP())`` and whose ``current_datatype`` is
            ``int``. The parameter list is always empty.

        Example:
            Current Unix timestamp as a SELECT column::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([Builtins.UnixNow()])
                # SELECT (UNIX_TIMESTAMP()) FROM `users`
                # -> [(1710513042,)]

            Compute account age in seconds without any date parsing::

                age_secs = Builtins.UnixNow() - Builtins.UnixEpoch(users.created_at)
                rows = users.get_row([users.username, age_secs])
                # -> [('Alice', 6394842), ('Bob', 1036800), ...]

            Filter users who signed up in the last 24 hours::

                rows = users.get_row(
                    [users.username, users.created_at],
                    where=Builtins.UnixNow()
                        - Builtins.UnixEpoch(users.created_at) < 86400,
                )
                # SELECT `users`.`username`, `users`.`created_at` FROM `users`
                # WHERE (((UNIX_TIMESTAMP()) - (UNIX_TIMESTAMP(`users`.`created_at`))) < %s)
                # Parameters: [86400]

            Generate a per-request cache key — combine with a user id::

                cache_key = Builtins.Concat(
                    'user:',
                    Builtins.Str(users.id),
                    ':',
                    Builtins.Str(Builtins.UnixNow()),
                )
                rows = users.get_row([cache_key])
                # -> [('user:42:1710513042',), ...]

            Store the current epoch in an UPDATE — useful for
            portability-friendly integer columns::

                users.update(
                    update={users.last_login_epoch: Builtins.UnixNow()},
                    where=users.id == 42,
                )
                # UPDATE `users` SET `last_login_epoch`=(UNIX_TIMESTAMP())
                # WHERE (`users`.`id` = %s);
                # Parameters: [42]

            Compute a future expiry — a session token valid for 1 hour::

                expires_at = Builtins.UnixNow() + 3600
                rows = users.get_row([users.id, expires_at])
                # -> [(1, 1710516642), ...]

            Set a session expiry during an UPDATE and later check it
            with a plain integer comparison::

                users.update(
                    update={users.token_expires: Builtins.UnixNow() + 3600},
                    where=users.id == 42,
                )

                # Later, filter expired sessions using a Python-side
                # timestamp — the comparison is a plain integer one:
                import time
                now_epoch = int(time.time())
                expired = users.get_row(
                    [users.id],
                    where=users.token_expires < now_epoch,
                )
        """
        return Builtins._make('(UNIX_TIMESTAMP())', [], int, Builtins._NullCol)

    # ================================================================ UnixEpoch

    @staticmethod
    def UnixEpoch(value):
        """Convert a date/time value to a Unix timestamp using ``UNIX_TIMESTAMP(x)``.

        Generates a ``UNIX_TIMESTAMP(<expr>)`` expression. The result is
        an INTEGER equal to the number of whole seconds since the Unix
        epoch (midnight UTC on 1 January 1970). The input must be a
        valid ``DATE``/``DATETIME``/``TIMESTAMP`` value or an ISO
        formatted string.

        .. note::
            ``UNIX_TIMESTAMP(<expr>)`` interprets the input in the
            server's session time zone by default. If your column stores
            UTC but the server runs in a non-UTC zone, the returned epoch
            will be offset. Normalise the column to UTC first with
            ``CONVERT_TZ`` if this matters.

        The literal string ``'now'`` is special-cased to emit
        ``UNIX_TIMESTAMP()`` — the same value :meth:`UnixNow` produces —
        so the SQLite calling convention still works.

        Args:
            value: The date/time expression to convert. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string — bound as a ``%s`` placeholder.
                - The literal string ``'now'`` — special-cased to
                  ``UNIX_TIMESTAMP()``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(UNIX_TIMESTAMP(<expr>))`` (or ``(UNIX_TIMESTAMP())`` for
            the ``'now'`` case) and whose ``current_datatype`` is
            ``int``.

        Example:
            Display the raw epoch value alongside a timestamp::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row([
                    users.created_at,
                    Builtins.UnixEpoch(users.created_at),
                ])
                # SELECT `users`.`created_at`,
                #        (UNIX_TIMESTAMP(`users`.`created_at`))
                # FROM `users`
                # -> [('2024-03-15 09:30:42', 1710495042), ...]

            Compute elapsed seconds since signup::

                elapsed = Builtins.UnixNow() - Builtins.UnixEpoch(users.created_at)
                rows = users.get_row([users.username, elapsed])
                # SELECT `users`.`username`,
                #        ((UNIX_TIMESTAMP()) - (UNIX_TIMESTAMP(`users`.`created_at`)))
                # FROM `users`

            Elapsed days — divide by 86400::

                days = (Builtins.UnixNow()
                        - Builtins.UnixEpoch(users.created_at)) / 86400
                rows = users.get_row([users.username, days])

            Filter by an absolute cutoff timestamp::

                cutoff = 1704067200   # 2024-01-01 00:00:00 UTC
                rows = users.get_row(
                    [users.username],
                    where=Builtins.UnixEpoch(users.created_at) > cutoff,
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE ((UNIX_TIMESTAMP(`users`.`created_at`)) > %s)
                # Parameters: [1704067200]

            Store a timestamp as an integer during an UPDATE::

                users.update(
                    update={users.last_seen_epoch: Builtins.UnixNow()},
                    where=users.id == 42,
                )

            Sort by absolute moment::

                rows = users.get_row(
                    [users.username],
                    order_by=Builtins.UnixEpoch(users.created_at),
                )

            The ``'now'`` special case mirrors :meth:`UnixNow`::

                rows = users.get_row([
                    Builtins.UnixEpoch('now'),   # -> (UNIX_TIMESTAMP()) current epoch
                ])
                # -> [(1710513042,)]

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.UnixEpoch('2024-03-15 00:00:00'),
                    # -> (UNIX_TIMESTAMP(%s)) with param '2024-03-15 00:00:00'
                    # -> 1710460800 (if session tz is UTC)
                ])
        """
        if Builtins._is_now(value):
            return Builtins._make('(UNIX_TIMESTAMP())', [], int, Builtins._NullCol)
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(UNIX_TIMESTAMP({sql}))', p, int, c)

    # ============================================================ date extractors

    @staticmethod
    def Year(value):
        """Extract the four-digit year from a date/time value using ``YEAR()``.

        Generates a ``YEAR(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1000 through 9999 (MySQL's ``DATE``
        type limit), or 0 for the ``'0000-00-00'`` sentinel when
        ``NO_ZERO_DATE`` is disabled.

        This is the SQL equivalent of ``datetime.date.year`` on a parsed
        Python date.

        Args:
            value: The date/time expression whose year is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(YEAR(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter rows by year::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Year(users.created_at) == 2024,
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE ((YEAR(`users`.`created_at`)) = %s)
                # Parameters: [2024]

            Year-over-year reporting — group in Python after fetching::

                rows = users.get_row([Builtins.Year(users.created_at)])
                from collections import Counter
                by_year = Counter(y for (y,) in rows)
                # {2022: 120, 2023: 340, 2024: 88}

            Extract the year as a SELECT column::

                rows = users.get_row([
                    users.username,
                    Builtins.Year(users.created_at),
                ])
                # SELECT `users`.`username`, (YEAR(`users`.`created_at`))
                # FROM `users`

            Range filter across years::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.Between(Builtins.Year(users.created_at), 2023, 2024),
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE (((YEAR(`users`.`created_at`))) BETWEEN %s AND %s)
                # Parameters: [2023, 2024]

            Compare years with arithmetic — stays numeric because
            ``current_datatype`` is ``int``::

                next_year = Builtins.Year(users.created_at) + 1
                rows = users.get_row([users.id, next_year])
                # SELECT `users`.`id`, ((YEAR(`users`.`created_at`)) + %s)
                # FROM `users`
                # Parameters: [1]

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Year('2024-03-15'),   # -> 2024
                    Builtins.Year('now'),          # -> current year
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(YEAR({sql}))', p, int, c)

    @staticmethod
    def Month(value):
        """Extract the month number (1-12) from a date/time value using ``MONTH()``.

        Generates a ``MONTH(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1 through 12, so it compares correctly
        with numeric literals (``== 3`` rather than ``== '03'``).

        This is the SQL equivalent of ``datetime.date.month`` on a
        parsed Python date.

        Args:
            value: The date/time expression whose month is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(MONTH(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter by month — pick a specific month regardless of year::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Month(users.created_at) == 3,
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE ((MONTH(`users`.`created_at`)) = %s)
                # Parameters: [3]
                # -> everything created in March, any year

            Seasonal filter — Q1 only::

                q1 = Builtins.Between(Builtins.Month(users.created_at), 1, 3)
                rows = users.get_row(
                    [users.username],
                    where=q1,
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE (((MONTH(`users`.`created_at`))) BETWEEN %s AND %s)
                # Parameters: [1, 3]

            Monthly histogram — compute in Python after fetching::

                rows = users.get_row([Builtins.Month(users.created_at)])
                from collections import Counter
                by_month = Counter(m for (m,) in rows)
                # {1: 45, 2: 38, 3: 52, ...} — a 12-bucket histogram

            Order by month — chronological within the year::

                rows = users.get_row(
                    [users.username, users.created_at],
                    order_by=Builtins.Month(users.created_at),
                )

            Select with formatted month name — combine with IIf for the
            common "Jan / Feb / ..." display::

                name = Builtins.IIf(
                    Builtins.Month(users.created_at) == 1, 'Jan',
                    Builtins.IIf(
                        Builtins.Month(users.created_at) == 2, 'Feb',
                        Builtins.IIf(
                            Builtins.Month(users.created_at) == 3, 'Mar',
                            'other',
                        ),
                    ),
                )
                rows = users.get_row([users.id, name])

            Filter on a computed month — e.g. users whose signup month
            matches their birth month::

                rows = users.get_row(
                    [users.username],
                    where=Builtins.Month(users.created_at)
                        == Builtins.Month(users.birth_date),
                )
                # SELECT `users`.`username` FROM `users`
                # WHERE ((MONTH(`users`.`created_at`)) = (MONTH(`users`.`birth_date`)))

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Month('2024-03-15'),   # -> 3
                    Builtins.Month('now'),          # -> current month 1..12
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(MONTH({sql}))', p, int, c)

    @staticmethod
    def Day(value):
        """Extract the day of the month (1-31) from a date/time value using ``DAY()``.

        Generates a ``DAY(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1 through 31, so it compares correctly
        with numeric literals.

        MySQL also accepts ``DAYOFMONTH()`` as a synonym; this helper
        uses ``DAY()`` for brevity. See :meth:`DayOfYear` for the ordinal
        day within the year.

        Args:
            value: The date/time expression whose day of month is
                extracted. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DAY(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Find rows created on the first of the month::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='app')
                invoices = db.invoices

                rows = invoices.get_row(
                    [invoices.id, invoices.issued_at, invoices.total],
                    where=Builtins.Day(invoices.issued_at) == 1,
                )
                # SELECT `invoices`.`id`, `invoices`.`issued_at`,
                #        `invoices`.`total`
                # FROM `invoices`
                # WHERE ((DAY(`invoices`.`issued_at`)) = %s)
                # Parameters: [1]

            Billing-cycle filter — mid-month settlements, days 13 to 15::

                rows = invoices.get_row(
                    [invoices.id],
                    where=Builtins.Between(
                        Builtins.Day(invoices.issued_at), 13, 15
                    ),
                )

            Detect weekend-adjacent days — days 1 or 2 of any month::

                rows = invoices.get_row(
                    [invoices.id, invoices.issued_at],
                    where=Builtins.Day(invoices.issued_at) <= 2,
                )

            Day-of-month histogram::

                rows = invoices.get_row([Builtins.Day(invoices.issued_at)])
                from collections import Counter
                by_day = Counter(d for (d,) in rows)
                # {1: 45, 2: 12, ..., 31: 3}

            End-of-month filter — days 28 through 31::

                rows = invoices.get_row(
                    [invoices.id],
                    where=Builtins.Between(
                        Builtins.Day(invoices.issued_at), 28, 31
                    ),
                )

            Compare day-of-month across two date columns::

                same_day = (Builtins.Day(invoices.issued_at)
                            == Builtins.Day(invoices.due_at))
                rows = invoices.get_row([invoices.id], where=same_day)

            Applied to a raw literal::

                rows = invoices.get_row([
                    Builtins.Day('2024-03-15'),   # -> 15
                    Builtins.Day('now'),          # -> current day 1..31
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(DAY({sql}))', p, int, c)

    @staticmethod
    def Hour(value):
        """Extract the hour (0-23) from a date/time value using ``HOUR()``.

        Generates a ``HOUR(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 0 through 23, so it compares correctly
        with numeric literals and works well with range filters.

        MySQL's ``HOUR`` uses a 24-hour clock. Midnight is 0, noon is 12,
        11 PM is 23.

        Args:
            value: The date/time expression whose hour is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and
                  parameters are reused.
                - :class:`Column` — the fully qualified column name is
                  used.
                - A raw Python string or number — bound as a ``%s``
                  placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(HOUR(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Business-hours filter — signups between 9 AM and 5 PM::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                hours = Builtins.Hour(users.created_at)
                rows = users.get_row(
                    [users.id, users.created_at],
                    where=(hours >= 9) & (hours < 17),
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE (((HOUR(`users`.`created_at`)) >= %s)
                #        AND ((HOUR(`users`.`created_at`)) < %s))
                # Parameters: [9, 17]

            Time-of-day bucketing — morning / afternoon / evening /
            night::

                bucket = Builtins.IIf(
                    Builtins.Hour(users.created_at) < 6, 'night',
                    Builtins.IIf(
                        Builtins.Hour(users.created_at) < 12, 'morning',
                        Builtins.IIf(
                            Builtins.Hour(users.created_at) < 18, 'afternoon',
                            'evening',
                        ),
                    ),
                )
                rows = users.get_row([users.id, bucket])

            Hourly histogram — a 24-bucket count of signups::

                rows = users.get_row([Builtins.Hour(users.created_at)])
                from collections import Counter
                by_hour = Counter(h for (h,) in rows)
                # {0: 3, 1: 1, ..., 9: 45, 10: 52, ..., 23: 8}
                # Great for finding the quietest hour to run maintenance.

            Filter by hour range with a wrap-around — e.g. "off-hours"
            (22:00 through 05:59)::

                h = Builtins.Hour(users.created_at)
                off_hours = (h >= 22) | (h < 6)
                rows = users.get_row([users.id], where=off_hours)

            Order by hour-of-day — clusters activity by time of day
            regardless of date::

                rows = users.get_row(
                    [users.username, users.created_at],
                    order_by=Builtins.Hour(users.created_at),
                )

            Compare hours across two columns — same-hour activity::

                same_hour = (Builtins.Hour(users.created_at)
                             == Builtins.Hour(users.last_login))
                rows = users.get_row([users.id], where=same_hour)

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Hour('2024-03-15 09:30:42'),   # -> 9
                    Builtins.Hour('now'),                    # -> current hour 0..23
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(HOUR({sql}))', p, int, c)

    @staticmethod
    def Minute(value):
        """Extract the minute (0-59) from a date/time value using MySQL's ``MINUTE()``.

        Generates a ``MINUTE(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 0 through 59, so it compares correctly with
        numeric literals and works well with range filters.

        This is the SQL equivalent of ``datetime.datetime.minute`` on a parsed
        Python datetime.

        Args:
            value: The date/time expression whose minute is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(MINUTE(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter rows created in the first fifteen minutes of any hour::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Minute(users.created_at) < 15,
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE ((MINUTE(`users`.`created_at`)) < %s)
                # Parameters: [15]

            Minute histogram — a 60-bucket distribution::

                rows = users.get_row([Builtins.Minute(users.created_at)])
                from collections import Counter
                by_minute = Counter(m for (m,) in rows)
                # {0: 5, 1: 3, ..., 59: 2}

            Compare minutes across two columns — same minute-of-hour::

                same_minute = (Builtins.Minute(users.created_at)
                               == Builtins.Minute(users.last_login))
                rows = users.get_row([users.id], where=same_minute)

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Minute('2024-03-15 09:30:42'),   # -> 30
                    Builtins.Minute('now'),                    # -> current 0..59
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(MINUTE({sql}))', p, int, c)

    @staticmethod
    def Second(value):
        """Extract the second (0-59) from a date/time value using MySQL's ``SECOND()``.

        Generates a ``SECOND(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 0 through 59, so it compares correctly with
        numeric literals. Fractional seconds (if the column was declared with
        ``DATETIME(n)``) are truncated — use ``MICROSECOND()`` via
        ``Builtins.Func`` if you need sub-second precision.

        This is the SQL equivalent of ``datetime.datetime.second`` on a parsed
        Python datetime.

        Args:
            value: The date/time expression whose second is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(SECOND(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter rows created in the first 10 seconds of any minute::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Second(users.created_at) <= 10,
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE ((SECOND(`users`.`created_at`)) <= %s)
                # Parameters: [10]

            Detect round-minute timestamps — a common data-quality check::

                on_the_minute = (Builtins.Second(users.created_at) == 0)
                rows = users.get_row([users.id], where=on_the_minute)

            Second histogram — 60-bucket distribution::

                rows = users.get_row([Builtins.Second(users.created_at)])
                from collections import Counter
                by_second = Counter(s for (s,) in rows)

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Second('2024-03-15 09:30:42'),   # -> 42
                    Builtins.Second('now'),                    # -> current 0..59
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(SECOND({sql}))', p, int, c)

    @staticmethod
    def DayOfWeek(value):
        """Return the day of the week using MySQL's ``DAYOFWEEK()`` — 1=Sunday … 7=Saturday.

        Generates a ``DAYOFWEEK(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1 through 7, using MySQL's convention where
        Sunday is 1 and Saturday is 7 (this follows the ODBC standard).

        .. warning::
            The numbering differs from both Python and ISO-8601:

            ===========  ============  ===========  ========
            Day          MySQL (1-7)   Python (0-6) ISO (1-7)
            ===========  ============  ===========  ========
            Sunday       1             6            7
            Monday       2             0            1
            Tuesday      3             1            2
            Wednesday    4             2            3
            Thursday     5             3            4
            Friday       6             4            5
            Saturday     7             5            6
            ===========  ============  ===========  ========

            If you need Python's numbering (0=Monday … 6=Sunday), use
            :meth:`Weekday`. If you need ISO-8601 numbering (1=Monday …
            7=Sunday), use :meth:`IsoWeekday`.

        Args:
            value: The date/time expression whose day-of-week is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DAYOFWEEK(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter only weekend rows (Saturday=7 or Sunday=1)::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                weekend = (Builtins.DayOfWeek(users.created_at) == 1) | \
                          (Builtins.DayOfWeek(users.created_at) == 7)
                rows = users.get_row([users.id, users.created_at], where=weekend)
                # SELECT ... WHERE (((DAYOFWEEK(`users`.`created_at`)) = %s)
                #                 OR ((DAYOFWEEK(`users`.`created_at`)) = %s))
                # Parameters: [1, 7]

            Group by weekday via IIf — turn numbers into names::

                name = Builtins.IIf(
                    Builtins.DayOfWeek(users.created_at) == 1, 'Sun',
                    Builtins.IIf(
                        Builtins.DayOfWeek(users.created_at) == 2, 'Mon',
                        Builtins.IIf(
                            Builtins.DayOfWeek(users.created_at) == 6, 'Fri',
                            'Other',
                        ),
                    ),
                )
                rows = users.get_row([users.id, name])

            Histogram over the 7 days::

                rows = users.get_row([Builtins.DayOfWeek(users.created_at)])
                from collections import Counter
                by_dow = Counter(d for (d,) in rows)
                # {1: 120, 2: 340, ..., 7: 88}

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.DayOfWeek('2024-03-15'),   # Friday -> 6
                    Builtins.DayOfWeek('now'),          # current 1..7
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(DAYOFWEEK({sql}))', p, int, c)

    @staticmethod
    def DayOfYear(value):
        """Return the ordinal day of the year (1-366) using MySQL's ``DAYOFYEAR()``.

        Generates a ``DAYOFYEAR(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1 through 366 (366 only for leap years). This
        is useful for building year-over-year comparisons and seasonal bucketing.

        This is the SQL equivalent of Python's
        ``datetime.date.timetuple().tm_yday``.

        Args:
            value: The date/time expression whose ordinal day is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DAYOFYEAR(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter the first week of the year::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.DayOfYear(users.created_at) <= 7,
                )
                # SELECT `users`.`id`, `users`.`created_at` FROM `users`
                # WHERE ((DAYOFYEAR(`users`.`created_at`)) <= %s)
                # Parameters: [7]

            Day-of-year histogram for seasonal analysis::

                rows = users.get_row([Builtins.DayOfYear(users.created_at)])
                from collections import Counter
                by_doy = Counter(d for (d,) in rows)

            Leap-year check — a year has a Feb 29 row if some row maps to
            day 60::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.DayOfYear(users.created_at) == 60,
                )

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.DayOfYear('2024-03-15'),   # -> 75
                    Builtins.DayOfYear('now'),          # -> current 1..366
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(DAYOFYEAR({sql}))', p, int, c)

    @staticmethod
    def WeekOfYear(value):
        """Return the ISO-8601 week number (1-53) using MySQL's ``WEEKOFYEAR()``.

        Generates a ``WEEKOFYEAR(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 1 through 53. Week 1 is the first week that
        contains a Thursday (ISO-8601 rule), and weeks start on Monday.

        This is the SQL equivalent of Python's
        ``datetime.date.isocalendar()[1]``.

        .. note::
            ``WEEKOFYEAR()`` is equivalent to ``WEEK(<expr>, 3)`` — mode 3 is
            the ISO-8601 mode. Do not confuse it with ``WEEK(<expr>)`` whose
            default mode depends on the ``default_week_format`` server
            variable.

        Args:
            value: The date/time expression whose ISO week number is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(WEEKOFYEAR(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Weekly signup reports::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                rows = users.get_row(
                    [Builtins.WeekOfYear(users.created_at)],
                )
                from collections import Counter
                by_week = Counter(w for (w,) in rows)
                # {1: 45, 2: 38, ..., 52: 12}

            Filter Q1 weeks only::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.WeekOfYear(users.created_at) <= 13,
                )
                # SELECT `users`.`id` FROM `users`
                # WHERE ((WEEKOFYEAR(`users`.`created_at`)) <= %s)
                # Parameters: [13]

            Compare the same ISO week across years — for the same calendar
            alignment::

                rows = users.get_row(
                    [Builtins.WeekOfYear(users.created_at)],
                    where=Builtins.WeekOfYear(users.created_at) == 15,
                )

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.WeekOfYear('2024-03-15'),   # -> 11
                    Builtins.WeekOfYear('now'),          # current 1..53
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(WEEKOFYEAR({sql}))', p, int, c)

    @staticmethod
    def Weekday(value):
        """Return Python's weekday number (0=Monday … 6=Sunday) via MySQL's ``WEEKDAY()``.

        Generates a ``WEEKDAY(<expr>)`` expression. The result is always an
        ``INTEGER`` in the range 0 through 6, matching Python's
        ``datetime.date.weekday()`` convention: Monday is 0, Sunday is 6.

        This is the safest day-of-week helper to pair with Python-side logic
        since the numbering is identical to Python's.

        ===========  =============  ========
        Day          ``Weekday``     Python
        ===========  =============  ========
        Monday       0              0
        Tuesday      1              1
        Wednesday    2              2
        Thursday     3              3
        Friday       4              4
        Saturday     5              5
        Sunday       6              6
        ===========  =============  ========

        Args:
            value: The date/time expression whose weekday is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(WEEKDAY(<expr>))`` and whose ``current_datatype`` is always
            ``int``.

        Example:
            Filter only weekends (Sat=5 or Sun=6)::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                weekend = (Builtins.Weekday(users.created_at) == 5) | \
                          (Builtins.Weekday(users.created_at) == 6)
                rows = users.get_row([users.id], where=weekend)

            Business-day filter — Monday through Friday::

                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Weekday(users.created_at) <= 4,
                )
                # SELECT ... WHERE ((WEEKDAY(`users`.`created_at`)) <= %s)
                # Parameters: [4]

            Histogram — 7 buckets in the same order as Python::

                rows = users.get_row([Builtins.Weekday(users.created_at)])
                from collections import Counter
                by_dow = Counter(d for (d,) in rows)
                # {0: Mon, 1: Tue, ..., 6: Sun}

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Weekday('2024-03-15'),   # Friday -> 4
                    Builtins.Weekday('now'),          # 0..6
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(WEEKDAY({sql}))', p, int, c)

    @staticmethod
    def IsoWeekday(value):
        """Return the ISO-8601 weekday number (1=Monday … 7=Sunday).

        MySQL has no dedicated ``ISODAYOFWEEK()`` function, so this helper
        composes one from ``WEEKDAY()``:

        .. code-block:: sql

            (WEEKDAY(<expr>) + 1)

        ``WEEKDAY()`` returns 0 for Monday through 6 for Sunday; adding 1
        shifts the range to 1 through 7, matching the ISO-8601 standard.

        Use this helper when you need to interoperate with ISO-8601 based
        libraries or when the numbering 1-7 reads more naturally than 0-6.

        Args:
            value: The date/time expression whose ISO weekday is extracted.
                Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``((WEEKDAY(<expr>) + 1))`` and whose ``current_datatype`` is
            always ``int``.

        Example:
            ISO weekday names via nested IIf::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                name = Builtins.IIf(
                    Builtins.IsoWeekday(users.created_at) == 1, 'Mon',
                    Builtins.IIf(
                        Builtins.IsoWeekday(users.created_at) == 7, 'Sun',
                        'Midweek',
                    ),
                )
                rows = users.get_row([users.id, name])

            Filter ISO weekends (Sat=6, Sun=7)::

                weekend = (Builtins.IsoWeekday(users.created_at) == 6) | \
                          (Builtins.IsoWeekday(users.created_at) == 7)
                rows = users.get_row([users.id], where=weekend)
                # SELECT `users`.`id` FROM `users`
                # WHERE ((((WEEKDAY(`users`.`created_at`) + %s)) = %s)
                #     OR (((WEEKDAY(`users`.`created_at`) + %s)) = %s))
                # Parameters: [1, 6, 1, 7]

            Filter the first three days of the ISO week::

                rows = users.get_row(
                    [users.id],
                    where=Builtins.IsoWeekday(users.created_at) <= 3,
                )

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.IsoWeekday('2024-03-15'),   # Friday -> 5
                    Builtins.IsoWeekday('now'),          # 1..7
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'((WEEKDAY({sql}) + 1))', p, int, c)

    @staticmethod
    def Strftime(fmt, value):
        """Format a date/time value using MySQL's ``DATE_FORMAT()``.

        Generates a ``DATE_FORMAT(<expr>, <fmt>)`` expression. The format
        string follows MySQL's percent-style specifiers, which differ from
        Python's ``strftime`` in a few places — most notably ``%i`` for
        minutes (Python uses ``%M``) and ``%s``/``%S`` for seconds.

        Common MySQL specifiers:

        =========  =========================
        Specifier  Meaning
        =========  =========================
        ``%Y``     Four-digit year
        ``%y``     Two-digit year
        ``%m``     Month, zero-padded (01-12)
        ``%c``     Month (1-12)
        ``%M``     Month name (January..December)
        ``%b``     Abbreviated month name
        ``%d``     Day of month, zero-padded
        ``%e``     Day of month
        ``%H``     Hour (00-23)
        ``%h``     Hour (01-12)
        ``%i``     Minutes (00-59)
        ``%s``     Seconds (00-59)
        ``%S``     Seconds (00-59) — synonym for ``%s``
        ``%W``     Weekday name (Sunday..Saturday)
        ``%a``     Abbreviated weekday
        ``%j``     Day of year (001-366)
        ``%T``     Time, 24-hour (hh:mm:ss)
        ``%r``     Time, 12-hour (hh:mm:ss AM/PM)
        =========  =========================

        This is the SQL counterpart of Python's ``strftime``, but with
        MySQL's specifiers. Note that the ``fmt`` argument is bound as a
        parameter (``%s``) — MySQL accepts it as a runtime string, so no
        injection risk is introduced.

        Args:
            fmt (str): The MySQL format string, e.g. ``'%Y-%m-%d'`` or
                ``'%H:%i:%s'``. It is bound as a parameter, so any string
                content is safe.
            value: The date/time expression to format. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DATE_FORMAT(<expr>, %s))`` with the format string appended to
            the parameter list, and whose ``current_datatype`` is always
            ``str``.

        Example:
            ISO-style date column::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                iso_date = Builtins.Strftime('%Y-%m-%d', users.created_at)
                rows = users.get_row([users.id, iso_date])
                # SELECT `users`.`id`, (DATE_FORMAT(`users`.`created_at`, %s))
                # FROM `users`
                # Parameters: ['%Y-%m-%d']
                # -> [(1, '2024-03-15'), ...]

            Human-readable month-year grouping::

                month = Builtins.Strftime('%M %Y', users.created_at)
                rows = users.get_row([month, Builtins.Count('*')])
                # SELECT (DATE_FORMAT(`users`.`created_at`, %s)), (COUNT(*))
                # FROM `users`
                # Parameters: ['%M %Y']
                # -> [('March 2024', 42), ('April 2024', 55), ...]

            Filename-safe timestamp::

                stamp = Builtins.Strftime('%Y%m%d_%H%i%s', users.created_at)
                rows = users.get_row([stamp])

            Sort by formatted string — useful for month names in calendar
            order (since ``%m`` is zero-padded, lexicographic order matches
            chronological order for the year-month combination)::

                rows = users.get_row(
                    [users.id],
                    order_by=Builtins.Strftime('%Y-%m', users.created_at),
                )

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.Strftime('%Y-%m-%d', '2024-03-15 09:30:42'),
                    # -> '2024-03-15'
                ])
        """
        sql, p, _, c = Builtins._normalize(value)
        return Builtins._make(f'(DATE_FORMAT({sql}, %s))', p + [fmt], str, c)

    _INTERVAL_RE = re.compile(
        r'^\s*([+-]?\d+)\s*'
        r'(day|days|week|weeks|month|months|year|years|hour|hours|minute|minutes|second|seconds)\s*$',
        re.IGNORECASE,
    )

    @staticmethod
    def _apply_modifiers(base_sql, base_params, modifiers):
        """Fold SQLite-style date modifiers into chained MySQL ``DATE_ADD``/``DATE_SUB`` calls.

        This internal helper takes a base SQL fragment and a sequence of
        modifier strings — the same style accepted by SQLite's ``date()``
        and ``datetime()`` functions — and produces the equivalent MySQL
        date arithmetic expression, along with the accumulated parameter
        list.

        Two kinds of modifiers are supported:

        * **Interval modifiers** — strings like ``'+1 day'``, ``'-3 hours'``,
          ``'+2 months'``, ``'-1 year'``. The integer sign determines whether
          ``DATE_ADD`` or ``DATE_SUB`` is emitted, and the unit is upper-cased
          and singularised (``'days'`` → ``'DAY'``). Each modifier wraps the
          previous SQL fragment, so modifiers apply left-to-right, matching
          SQLite's semantics.
        * **Anchor modifiers** — the special strings ``'start of month'``,
          ``'start of year'``, and ``'start of day'``. These rewrite the
          fragment using ``DATE_FORMAT`` (for month/year) or ``DATE()``
          (for day), truncating the value to the beginning of the period.

        Args:
            base_sql (str): The starting SQL fragment, e.g.
                ``'`users`.`created_at`'`` or ``'NOW()'``.
            base_params (list): The parameters already associated with
                ``base_sql``. Returned values are appended to a copy of this
                list; the input list is never modified.
            modifiers (tuple[str, ...]): One or more modifier strings, in
                the order they should be applied.

        Returns:
            tuple[str, list]: A two-element tuple ``(sql, params)`` where
            ``sql`` is the transformed fragment and ``params`` is the
            accumulated parameter list (a copy of ``base_params`` plus
            any new parameters added along the way — currently, the
            modifiers themselves carry no parameters).

        Raises:
            ValueError: If a modifier does not match the recognised
                interval pattern and is not one of the three anchor
                modifiers. The error message lists the accepted forms.

        Example:
            Internal usage::

                >>> Builtins._apply_modifiers('NOW()', [], ['+1 day'])
                ('DATE_ADD(NOW(), INTERVAL 1 DAY)', [])

                >>> Builtins._apply_modifiers('NOW()', [], ['+1 month', '-2 days'])
                ('DATE_SUB(DATE_ADD(NOW(), INTERVAL 1 MONTH), INTERVAL 2 DAY)', [])

                >>> Builtins._apply_modifiers('NOW()', [], ['start of month'])
                ("DATE_FORMAT(NOW(), '%Y-%m-01')", [])
        """
        params = list(base_params)
        sql    = base_sql
        for mod in modifiers:
            low = mod.strip().lower()
            if low == 'start of month':
                sql = f"CONCAT(YEAR({sql}), '-', LPAD(MONTH({sql}), 2, '0'), '-01')"
                params = params + params        # چون {sql} دو بار آمده
                continue
            if low == 'start of year':
                sql = f"CONCAT(YEAR({sql}), '-01-01')"
                continue
            if low == 'start of day':
                sql = f'DATE({sql})'
                continue
            m = Builtins._INTERVAL_RE.match(mod)
            if not m:
                raise ValueError(
                    f"Unrecognized modifier {mod!r}. Use something like "
                    f"'+1 day', '-2 hours', '+3 months', 'start of month'."
                )
            n    = int(m.group(1))
            unit = m.group(2).upper().rstrip('S')
            fn   = 'DATE_ADD' if n >= 0 else 'DATE_SUB'
            sql  = f'{fn}({sql}, INTERVAL {abs(n)} {unit})'
        return sql, params

    @staticmethod
    def DateAdd(value, *modifiers):
        """Add or subtract intervals from a date value — returns a DATE.

        Generates a MySQL date arithmetic expression that applies one or
        more SQLite-style modifiers to ``value`` and returns the resulting
        date (truncated to day precision) as a TEXT string in
        ``'YYYY-MM-DD'`` format.

        Two kinds of modifiers are supported:

        * **Interval modifiers** — ``'+1 day'``, ``'-3 hours'``, ``'+2 months'``,
          ``'-1 year'``, etc. Positive values use ``DATE_ADD``; negative
          values use ``DATE_SUB``. Modifiers apply left-to-right.
        * **Anchor modifiers** — ``'start of month'``, ``'start of year'``,
          ``'start of day'``. These truncate the value to the beginning of
          the period.

        The literal string ``'now'`` is special-cased to ``CURDATE()`` — the
        current date — so the SQLite calling convention still works.

        The final result is always wrapped in ``DATE(...)``, discarding any
        time-of-day component, matching SQLite's ``date()`` semantics.

        Args:
            value: The starting date/datetime. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.
                - The literal string ``'now'`` — special-cased to ``CURDATE()``.

            *modifiers: Zero or more modifier strings applied in order.
                If no modifiers are given, the value is only normalised to a
                date.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is the
            composed SQL fragment wrapped in ``DATE(...)`` and whose
            ``current_datatype`` is always ``str``.

        Raises:
            ValueError: If any modifier is unrecognised — propagated from
                :meth:`_apply_modifiers`.

        Example:
            Tomorrow's date::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                tomorrow = Builtins.DateAdd('now', '+1 day')
                rows = users.get_row([tomorrow])
                # SELECT (DATE(DATE_ADD(CURDATE(), INTERVAL 1 DAY))) FROM `users`
                # -> [('2024-03-16',)]

            One week after signup::

                after_week = Builtins.DateAdd(users.created_at, '+7 days')
                rows = users.get_row([users.id, after_week])
                # SELECT `users`.`id`,
                #        (DATE(DATE_ADD(`users`.`created_at`, INTERVAL 7 DAY)))
                # FROM `users`

            First day of the current month::

                month_start = Builtins.DateAdd('now', 'start of month')
                rows = users.get_row([month_start])
                # SELECT (DATE(DATE_FORMAT(CURDATE(), '%Y-%m-01'))) FROM `users`
                # -> [('2024-03-01',)]

            Chained modifiers — one month ago, then start of month::

                prev_month_start = Builtins.DateAdd('now', '-1 month', 'start of month')
                rows = users.get_row([prev_month_start])
                # -> [('2024-02-01',)]

            Filter users created in the last 30 days::

                cutoff = Builtins.DateAdd('now', '-30 days')
                rows = users.get_row(
                    [users.id, users.created_at],
                    where=Builtins.Date(users.created_at) >= cutoff,
                )
                # SELECT ... WHERE ((DATE(`users`.`created_at`)) >=
                #   (DATE(DATE_SUB(CURDATE(), INTERVAL 30 DAY))))

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.DateAdd('2024-03-15', '+1 month'),   # -> '2024-04-15'
                ])
        """
        if Builtins._is_now(value):
            sql, p, c = 'CURDATE()', [], Builtins._NullCol
        else:
            sql, p, _, c = Builtins._normalize(value)
        if not modifiers:
            return Builtins._make(f'(DATE({sql}))', p, str, c)
        inner, params = Builtins._apply_modifiers(sql, p, modifiers)
        return Builtins._make(f'(DATE({inner}))', params, str, c)

    @staticmethod
    def DateTimeAdd(value, *modifiers):
        """Add or subtract intervals from a datetime value — returns a DATETIME.

        The datetime counterpart of :meth:`DateAdd`. Applies the same set
        of SQLite-style modifiers to ``value`` but preserves the time-of-day
        component and normalises the result to a DATETIME value as a TEXT
        string in ``'YYYY-MM-DD HH:MM:SS'`` format.

        The literal string ``'now'`` is special-cased to ``NOW()`` — the
        current datetime — so the SQLite calling convention still works.

        When no modifiers are given, the value is only normalised to a
        DATETIME via ``CAST(<expr> AS DATETIME)``.

        Args:
            value: The starting datetime. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.
                - The literal string ``'now'`` — special-cased to ``NOW()``.

            *modifiers: Zero or more modifier strings applied in order.
                Recognised forms are the same as for :meth:`DateAdd`.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is the
            composed SQL fragment (either a bare ``DATE_ADD``/``DATE_SUB``
            chain or a ``CAST(... AS DATETIME)`` when no modifiers are
            given) and whose ``current_datatype`` is always ``str``.

        Raises:
            ValueError: If any modifier is unrecognised — propagated from
                :meth:`_apply_modifiers`.

        Example:
            One hour from now::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='app')
                events = db.events

                soon = Builtins.DateTimeAdd('now', '+1 hour')
                rows = events.get_row([soon])
                # SELECT (DATE_ADD(NOW(), INTERVAL 1 HOUR)) FROM `events`
                # -> [('2024-03-15 15:30:42',)]

            Filter events in the last 24 hours::

                cutoff = Builtins.DateTimeAdd('now', '-1 day')
                rows = events.get_row(
                    [events.id, events.occurred_at],
                    where=Builtins.DateTime(events.occurred_at) >= cutoff,
                )

            A token expiring in 30 minutes::

                expires = Builtins.DateTimeAdd('now', '+30 minutes')
                rows = events.get_row([expires])

            Normalise a date-only string to a full datetime::

                normalised = Builtins.DateTimeAdd('2024-03-15')
                rows = events.get_row([normalised])
                # SELECT (CAST(%s AS DATETIME)) FROM `events`
                # Parameters: ['2024-03-15']
                # -> [('2024-03-15 00:00:00',)]

            Chain multiple modifiers — one month ahead, then anchor to
            the first day of that month (with time preserved as 00:00:00)::

                first_of_next_month = Builtins.DateTimeAdd(
                    'now', '+1 month', 'start of month'
                )
                rows = events.get_row([first_of_next_month])
        """
        if Builtins._is_now(value):
            sql, p, c = 'NOW()', [], Builtins._NullCol
        else:
            sql, p, _, c = Builtins._normalize(value)
        if not modifiers:
            return Builtins._make(f'(CAST({sql} AS DATETIME))', p, str, c)
        inner, params = Builtins._apply_modifiers(sql, p, modifiers)
        return Builtins._make(f'({inner})', params, str, c)

    @staticmethod
    def TimeAdd(value, *modifiers):
        """Add or subtract intervals from a datetime value — returns a TIME.

        The time-of-day variant of :meth:`DateAdd` / :meth:`DateTimeAdd`.
        Applies the same modifier grammar but wraps the final result in
        ``TIME(...)``, extracting only the clock portion as a TEXT string
        in ``'HH:MM:SS'`` format.

        The literal string ``'now'`` is special-cased to ``NOW()`` — the
        current datetime — so the SQLite calling convention still works.
        Only the time component of the result is returned.

        When no modifiers are given, the value is only normalised to a
        TIME via ``TIME(<expr>)``.

        Args:
            value: The starting datetime. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.
                - The literal string ``'now'`` — special-cased to ``NOW()``.

            *modifiers: Zero or more modifier strings applied in order.
                Recognised forms are the same as for :meth:`DateAdd`.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is either
            ``(TIME(<expr>))`` when no modifiers are given, or
            ``(TIME(<date_arithmetic_chain>))`` otherwise, and whose
            ``current_datatype`` is always ``str``.

        Raises:
            ValueError: If any modifier is unrecognised — propagated from
                :meth:`_apply_modifiers`.

        Example:
            Time in 2 hours from now, as a clock time only::

                from Ormophine.Mysql import Driver, Builtins

                db     = Driver(host='localhost', port=3306, username='root',
                                password='secret', db_name='app')
                events = db.events

                in_two_hours = Builtins.TimeAdd('now', '+2 hours')
                rows = events.get_row([in_two_hours])
                # SELECT (TIME(DATE_ADD(NOW(), INTERVAL 2 HOUR))) FROM `events`
                # -> [('17:30:42',)]

            30 minutes after a scheduled time::

                later = Builtins.TimeAdd(events.scheduled_at, '+30 minutes')
                rows = events.get_row([events.id, later])
                # SELECT `events`.`id`,
                #        (TIME(DATE_ADD(`events`.`scheduled_at`, INTERVAL 30 MINUTE)))
                # FROM `events`

            Strip the date from a datetime column, leaving only the time::

                time_only = Builtins.TimeAdd(events.occurred_at)
                rows = events.get_row([events.id, time_only])
                # SELECT `events`.`id`, (TIME(`events`.`occurred_at`))
                # FROM `events`

            Applied to a raw literal::

                rows = events.get_row([
                    Builtins.TimeAdd('2024-03-15 14:30:00', '+1 hour'),
                    # -> '15:30:00'
                ])
        """
        if Builtins._is_now(value):
            sql, p, c = 'NOW()', [], Builtins._NullCol
        else:
            sql, p, _, c = Builtins._normalize(value)
        if not modifiers:
            return Builtins._make(f'(TIME({sql}))', p, str, c)
        inner, params = Builtins._apply_modifiers(sql, p, modifiers)
        return Builtins._make(f'(TIME({inner}))', params, str, c)

    @staticmethod
    def StrftimeMod(fmt, value, *modifiers):
        """Format a date/time value after applying modifiers — a modifier-aware ``Strftime``.

        Combines :meth:`_apply_modifiers` with :meth:`Strftime`: applies the
        given SQLite-style modifiers to ``value``, then formats the result
        with the MySQL ``DATE_FORMAT()`` function using ``fmt``.

        This is the right helper when you need both shifting and formatting
        in one expression, e.g. "the month name of the previous month" or
        "the ISO date one week ahead".

        The literal string ``'now'`` is special-cased to ``NOW()`` — the
        current datetime — so the SQLite calling convention still works.

        Args:
            fmt (str): The MySQL ``DATE_FORMAT`` format string, e.g.
                ``'%Y-%m-%d'`` or ``'%H:%i:%s'``. Bound as a parameter.
            value: The starting date/datetime. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.
                - The literal string ``'now'`` — special-cased to ``NOW()``.

            *modifiers: Zero or more modifier strings applied before
                formatting. Recognised forms are the same as for
                :meth:`DateAdd`. When omitted, the method degenerates to
                :meth:`Strftime`.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DATE_FORMAT(<modified_expr>, %s))`` and whose
            ``current_datatype`` is always ``str``. The format string is
            appended to the parameter list after any parameters carried by
            ``value``.

        Raises:
            ValueError: If any modifier is unrecognised — propagated from
                :meth:`_apply_modifiers`.

        Example:
            The month name of the previous month::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                prev_month = Builtins.StrftimeMod('%M %Y', 'now', '-1 month')
                rows = users.get_row([prev_month])
                # SELECT (DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 MONTH), %s))
                # FROM `users`
                # Parameters: ['%M %Y']
                # -> [('February 2024',)]

            ISO date one week after each signup::

                due = Builtins.StrftimeMod(
                    '%Y-%m-%d', users.created_at, '+7 days'
                )
                rows = users.get_row([users.id, due])

            Group by month after shifting into the previous month::

                bucket = Builtins.StrftimeMod('%Y-%m', users.created_at, '-1 month')
                rows = users.get_row([bucket, Builtins.Count('*')])

            Normalise a mixed-format column to ISO date via a no-op
            modifier chain — equivalent to :meth:`Strftime`::

                iso = Builtins.StrftimeMod('%Y-%m-%d', users.created_at)
                rows = users.get_row([iso])

            Applied to a raw literal::

                rows = users.get_row([
                    Builtins.StrftimeMod(
                        '%Y-%m-%d', '2024-03-15', '+1 month'
                    ),
                    # -> '2024-04-15'
                ])
        """
        if Builtins._is_now(value):
            sql, p, c = 'NOW()', [], Builtins._NullCol
        else:
            sql, p, _, c = Builtins._normalize(value)
        if modifiers:
            sql, p = Builtins._apply_modifiers(sql, p, modifiers)
        return Builtins._make(f'(DATE_FORMAT({sql}, %s))', p + [fmt], str, c)

    @staticmethod
    def DateDiffDays(a, b):
        """Signed whole-day count between two dates using MySQL's ``DATEDIFF()``.

        Generates a ``DATEDIFF(<a>, <b>)`` expression. MySQL evaluates this
        as ``<a> - <b>`` in days: a positive result means ``a`` is later
        than ``b``, a negative result means ``a`` is earlier. Only the date
        portion of each operand is considered; the time-of-day is ignored,
        so ``DATEDIFF('2024-03-16', '2024-03-15 23:59:59')`` returns ``1``.

        The result is always an ``INTEGER``.

        .. note::
            Because MySQL's ``DATEDIFF`` truncates to the day, it does not
            give a fractional count. If you need sub-day precision, use
            :meth:`DateDiffSeconds` and divide by 86400.

        Args:
            a: The later (minuend) date expression. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

            b: The earlier (subtrahend) date expression. Same accepted
                types as ``a``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(DATEDIFF(<a>, <b>))`` and whose ``current_datatype`` is
            always ``int``. Parameters are concatenated left-to-right:
            first ``a``'s, then ``b``'s.

        Example:
            Account age in days::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                age_days = Builtins.DateDiffDays(Builtins.Now(), users.created_at)
                rows = users.get_row([users.username, age_days])
                # SELECT `users`.`username`,
                #        (DATEDIFF(NOW(), `users`.`created_at`))
                # FROM `users`
                # -> [('alice', 42), ('bob', 8), ...]

            Filter accounts older than 30 days::

                age_days = Builtins.DateDiffDays(Builtins.Now(), users.created_at)
                rows = users.get_row(
                    [users.id, users.username],
                    where=age_days > 30,
                )
                # SELECT ... WHERE ((DATEDIFF(NOW(), `users`.`created_at`)) > %s)
                # Parameters: [30]

            Signup cohort by weekly buckets::

                age_days = Builtins.DateDiffDays(Builtins.Now(), users.created_at)
                bucket   = Builtins.Floor(age_days / 7)
                rows = users.get_row([bucket, Builtins.Count('*')])

            Order by "newest first" via the reverse diff — the largest
            positive value corresponds to the most recent signup::

                rows = users.get_row(
                    [users.username],
                    order_by=Builtins.DateDiffDays(users.created_at, '1970-01-01') * -1,
                )

            Applied to raw literals::

                rows = users.get_row([
                    Builtins.DateDiffDays('2024-03-15', '2024-03-01'),   # -> 14
                    Builtins.DateDiffDays('2024-03-01', '2024-03-15'),   # -> -14
                ])
        """
        s1, p1, _, c = Builtins._normalize(a)
        s2, p2, _, _ = Builtins._normalize(b)
        return Builtins._make(f'(DATEDIFF({s1}, {s2}))', p1 + p2, int, c)

    @staticmethod
    def DateDiffSeconds(a, b):
        """Signed whole-second count between two moments using ``TIMESTAMPDIFF(SECOND, ...)``.

        Generates a ``TIMESTAMPDIFF(SECOND, <b>, <a>)`` expression. MySQL
        evaluates this as ``<a> - <b>`` in whole seconds: a positive result
        means ``a`` is later than ``b``, a negative result means ``a`` is
        earlier. Unlike :meth:`DateDiffDays`, the full datetime is used,
        including the time-of-day.

        The result is always an ``INTEGER`` — MySQL's ``TIMESTAMPDIFF``
        rounds toward zero for fractional seconds.

        The argument order is chosen so that ``DateDiffSeconds(later, earlier)``
        is positive, matching Python's ``(later - earlier).total_seconds()``
        idiom. Internally the call is emitted as ``TIMESTAMPDIFF(SECOND, b, a)``
        because MySQL's ``TIMESTAMPDIFF`` expects ``(unit, start, end)`` — the
        older timestamp first.

        Args:
            a: The later (end) moment. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

            b: The earlier (start) moment. Same accepted types as ``a``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(TIMESTAMPDIFF(SECOND, <b>, <a>))`` and whose
            ``current_datatype`` is always ``int``. Parameters are
            concatenated left-to-right: first ``a``'s, then ``b``'s.

        Example:
            Session duration in seconds — a common analytics metric::

                from Ormophine.Mysql import Driver, Builtins

                db       = Driver(host='localhost', port=3306, username='root',
                                  password='secret', db_name='analytics')
                sessions = db.sessions

                duration = Builtins.DateDiffSeconds(
                    sessions.ended_at, sessions.started_at
                )
                rows = sessions.get_row([sessions.id, duration])
                # SELECT `sessions`.`id`,
                #        (TIMESTAMPDIFF(SECOND, `sessions`.`started_at`,
                #                                `sessions`.`ended_at`))
                # FROM `sessions`

            Filter long sessions (> 1 hour)::

                duration = Builtins.DateDiffSeconds(
                    sessions.ended_at, sessions.started_at
                )
                rows = sessions.get_row(
                    [sessions.id],
                    where=duration > 3600,
                )

            Elapsed seconds since signup::

                users = db.users
                elapsed = Builtins.DateDiffSeconds(Builtins.Now(), users.created_at)
                rows = users.get_row([users.username, elapsed])
                # SELECT `users`.`username`,
                #        (TIMESTAMPDIFF(SECOND, `users`.`created_at`, NOW()))
                # FROM `users`

            Convert to fractional days by chaining arithmetic::

                elapsed = Builtins.DateDiffSeconds(Builtins.Now(), users.created_at)
                days = elapsed / 86400
                rows = users.get_row([users.username, days])

            Applied to raw literals::

                rows = sessions.get_row([
                    Builtins.DateDiffSeconds(
                        '2024-03-15 10:00:00', '2024-03-15 09:30:00'
                    ),   # -> 1800
                ])
        """
        s1, p1, _, c = Builtins._normalize(a)
        s2, p2, _, _ = Builtins._normalize(b)
        return Builtins._make(f'(TIMESTAMPDIFF(SECOND, {s2}, {s1}))', p2 + p1, int, c)

    @staticmethod
    def Timediff(a, b):
        """Elapsed time between two moments as an ``HH:MM:SS`` string using ``TIMEDIFF()``.

        Generates a ``TIMEDIFF(<a>, <b>)`` expression. MySQL returns a
        signed ``TIME`` value representing the elapsed time from ``b`` to
        ``a`` — i.e., ``<a> - <b>``. The result is a TEXT string in
        ``'HH:MM:SS'`` format (or ``'-HH:MM:SS'`` for negative results).

        Unlike :meth:`DateDiffSeconds`, which returns an integer count,
        ``TIMEDIFF`` keeps the value in the TIME domain, so it composes
        naturally with the ``TIME`` arithmetic and formatting helpers. It
        is useful for producing human-readable durations.

        .. note::
            MySQL's ``TIMEDIFF`` limits its result to the TIME range
            (``-838:59:59`` to ``838:59:59``). For durations that may
            exceed that range, use :meth:`DateDiffSeconds` and format the
            integer in Python.

        Args:
            a: The later (end) moment. Supported types:

                - :class:`ColumnsOperation` — its SQL fragment and parameters
                  are reused.
                - :class:`Column` — the fully qualified column name is used.
                - A raw Python string or number — bound as a ``%s`` placeholder.

            b: The earlier (start) moment. Same accepted types as ``a``.

        Returns:
            ColumnsOperation: An expression whose ``_output[0]`` is
            ``(TIMEDIFF(<a>, <b>))`` and whose ``current_datatype`` is
            always ``str``. Parameters are concatenated left-to-right:
            first ``a``'s, then ``b``'s.

        Example:
            Human-readable account age::

                from Ormophine.Mysql import Driver, Builtins

                db    = Driver(host='localhost', port=3306, username='root',
                               password='secret', db_name='app')
                users = db.users

                age_str = Builtins.Timediff(Builtins.Now(), users.created_at)
                rows = users.get_row([users.username, age_str])
                # SELECT `users`.`username`,
                #        (TIMEDIFF(NOW(), `users`.`created_at`))
                # FROM `users`
                # -> [('alice', '02 05:29:18'), ...]  # MySQL DAY+HH:MM:SS
                # Note: values exceeding 24h are formatted as `DD HH:MM:SS`

            Session duration in clock notation::

                duration = Builtins.Timediff(
                    sessions.ended_at, sessions.started_at
                )
                rows = sessions.get_row([sessions.id, duration])
                # -> [('01:23:45',), ...]

            Sort by duration — largest first via arithmetic negation::

                duration = Builtins.Timediff(
                    sessions.ended_at, sessions.started_at
                )
                rows = sessions.get_row(
                    [sessions.id],
                    order_by=duration * -1,
                )

            Combine with :meth:`Strftime` to get compact labels::

                duration = Builtins.Timediff(
                    sessions.ended_at, sessions.started_at
                )
                label = Builtins.Strftime('%H:%i', duration)
                rows = sessions.get_row([sessions.id, label])

            Applied to raw literals::

                rows = users.get_row([
                    Builtins.Timediff('2024-03-15 10:00:00',
                                      '2024-03-15 09:30:00'),   # -> '00:30:00'
                ])
        """
        s1, p1, _, c = Builtins._normalize(a)
        s2, p2, _, _ = Builtins._normalize(b)
        return Builtins._make(f'(TIMEDIFF({s1}, {s2}))', p1 + p2, str, c)    
    

from __future__ import annotations

class _NullCol:
    """
    Fallback ``col_obj`` used when a ColumnsOperation-shaped object has no
    originating Column.

    Supplies the same attribute surface as a real :class:`Column`
    (``datatype``, ``name``, ``first_name``, ``table_obj``) so that every
    method inherited from :class:`ColumnsOperation` keeps working when the
    expression is built from a raw literal rather than a real column.
    """
    datatype   = None
    name       = ''
    first_name = ''
    class table_obj:
        _PlaceHolder = type(None)   # Nothing isinstance-matches this


class ColumnsOperation:
    """
    Builds SQL expressions for column operations, enabling chainable arithmetic, string manipulation, comparisons, and logical conditions.

    This class is the core of the ORM's expression system. It is not typically
    instantiated directly by user code; instead, instances are returned by
    :class:`Column` objects when operators (``+``, ``-``, ``*``, ``/``, etc.)
    or methods (``.eq()``, ``.like()``, ``.upper()``, etc.) are applied to them.

    The class stores the generated SQL fragment and its associated parameter
    list in the internal ``_output`` attribute, which is a tuple of the form
    ``(sql_fragment, parameters)``. All methods that modify the expression
    update this tuple and return ``self``, allowing for fluent chaining.

    The SQL generation is context‑aware: for numeric columns, arithmetic
    operators produce standard SQL arithmetic (e.g., ``+``, ``-``, ``*``),
    while for string columns, the same operators produce string concatenation
    (``||``) or use appropriate functions like ``SUBSTRING`` and ``TRIM``.

    The class provides:
        - Arithmetic operators: ``+``, ``-``, ``*``, ``/``, ``%``, ``**`` (POW)
        - Comparison methods: ``.eq()``, ``.ne()``, ``.gt()``, ``.lt()``,
          ``.ge()``, ``.le()`` (and their dunder equivalents)
        - String methods: ``.like()``, ``.startswith()``, ``.endswith()``,
          ``.contains()``, ``.upper()``, ``.lower()``, ``.strip()``,
          ``.lstrip()``, ``.rstrip()``, ``.replace()``, ``.add_end()``,
          ``.add_first()``, and slice indexing via ``__getitem__``
        - Logical operators: ``&`` (AND), ``|`` (OR)
        - Set membership: ``.In()``

    Attributes:
        _output (tuple): A two‑element tuple ``(sql_fragment, parameters)``.
            The SQL fragment is a string with optional placeholder markers
            (``%s``) for parameters; the parameters list contains all values
            that will be substituted. Initially, this attribute is set to an
            empty string, but after any operation it becomes a tuple.
        col_obj (Column): The :class:`Column` object that this operation is
            associated with. Used to determine the column's datatype (string
            vs. numeric) when choosing the correct SQL operator.

    Example:
        Chaining operations to build a complex condition::

            from ormophine.Mysql import Table

            # Assume `users` is a Table instance with columns: id, name, age
            condition = (users.age > 18) & users.name.startswith('A')
            # condition._output[0] -> '((users.age > %s) AND (users.name like %s || '%%'))'
            # condition._output[1] -> [18, 'A']

            # Using string manipulation
            full_name = users.first_name.add_end(' ').add_end(users.last_name)
            # full_name._output[0] -> '((users.first_name || %s) || users.last_name)'
            # full_name._output[1] -> [' ']

    Note:
        All methods that modify the expression return the instance itself,
        enabling method chaining. The actual execution of the SQL is handled
        by :class:`Table` methods such as :meth:`~Table.get_row` or
        :meth:`~Table.update`, which accept a :class:`ColumnsOperation` as
        the ``where`` parameter.
    """
    def __init__(self, col_obj):
        """
        Initialize a new ColumnsOperation instance.

        This class represents a chainable operation on a column (or a combination
        of columns) that produces an SQL expression and its associated parameters.
        It is used internally by the :class:`Column` class to build complex
        expressions for queries, updates, and conditions. The :attr:`_output`
        attribute stores a tuple ``(sql_expression, param_list)`` that accumulates
        as operations are applied.

        Args:
            col_obj (Column): The :class:`Column` object that this operation is
                associated with. It provides the column name, table reference,
                and datatype, which influence how operations (e.g., addition,
                concatenation) are rendered in SQL.

        Returns:
            None

        Example:
            This class is typically used indirectly via :class:`Column` operators::

                # Assuming `users.age` is a Column
                expr = users.age + 5
                # `expr` is a ColumnsOperation instance

                # Applying further chained operations
                expr = (users.first_name + ' ' + users.last_name).upper()

            In each case, the internal SQL expression and parameters are built up
            to be used in a query or condition.
        """
        self._output = ('', []) # To apply operations in a chained manner
        self.col_obj = col_obj
        self.current_datatype = col_obj.datatype
        
    def __add__(self, other):
        """
        Add two values or expressions in a SQL context.

        This operator generates a SQL expression for addition (or string
        concatenation) between the current column/expression and another value.
        The operation performed depends on the column's datatype:

        - If the column datatype is ``str``, the SQL ``||`` concatenation
        operator is used (with appropriate MySQL ``PIPES_AS_CONCAT`` mode
        enabled).
        - Otherwise, the SQL ``+`` operator is used for numeric addition.

        The method supports chaining by mutating the internal ``_output`` tuple
        and returning ``self``.

        Args:
            other (Any): The value, column, or expression to add. Can be an
                instance of :class:`ColumnsOperation`, :class:`Column`, or a
                literal (``int``, ``float``, ``str``). For non-literal types,
                the appropriate SQL representation is generated.

        Returns:
            ColumnsOperation: The current instance with updated internal state,
                allowing method chaining.

        Raises:
            None: This method does not raise exceptions directly; however,
                underlying database errors may occur when the resulting SQL is
                executed.

        Example:
            Assuming a :class:`Column` named ``users.age`` with numeric datatype
            and ``users.first_name`` with string datatype::

                # Numeric addition
                expr = users.age + 5
                # Generates SQL: (`users`.`age` + 5)

                # String concatenation
                expr = users.first_name + ' ' + users.last_name
                # Generates SQL: (`users`.`first_name` || ' ' || `users`.`last_name`)

            The resulting :class:`ColumnsOperation` can be used in WHERE clauses,
            UPDATE assignments, or SELECT expressions.
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} {'||' if (self.current_datatype == str) or (other.current_datatype == str) else '+'} {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} {'||' if (self.current_datatype == str) or (other.datatype == str) else '+'} {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} {'||' if (self.current_datatype == str) else '+'} %s)', self._output[1]+[other]) if isinstance(other, int) or isinstance(other , float)  or isinstance(other, self.col_obj.table_obj._PlaceHolder) else (f'({self._output[0]} || %s)', self._output[1]+[other if isinstance(other, str) else str(other)])
        new_op.current_datatype = str if (isinstance(other, ColumnsOperation) and other.current_datatype == str) or (isinstance(other, Column) and other.datatype == str) or self.current_datatype == str or ( not isinstance(other, ColumnsOperation) and not isinstance(other,Column) and not isinstance(other, int) and not isinstance(other, float)) else self.current_datatype
        return new_op

    def __radd__(self, other):
        """
        Implement right-side addition (`other + self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of a ``+`` operator. It generates a SQL expression
        string and accumulates parameter values. The behavior depends on the
        type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using ``||`` (for string columns) or ``+`` (for numeric columns).
        - If ``other`` is a :class:`Column`, its name is used directly, and the
        operator is chosen based on the column's datatype (string vs. numeric).
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is collected.
        - If ``other`` is a string, it is treated as a string literal, using
        the ``||`` operator (string concatenation) and a placeholder.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float, str): The left operand
                to be added to this operation. Its type determines how the SQL
                expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a string column ``users.full_name`` and a numeric column
            ``users.age``::

                # Right addition with a string literal
                op = users.full_name + ' ' + users.last_name
                # The __radd__ is called for ' ' + users.last_name

                # Resulting SQL: (users.full_name || %s)

            For numeric columns::

                op = 100 + users.age
                # Resulting SQL: (%s + users.age)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({other._output[0]} {'||' if (self.current_datatype == str) or (other.current_datatype == str) else '+'} {self._output[0]})', other._output[1]+self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} {'||' if (self.current_datatype == str) or (other.datatype == str) else '+'} {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s {'||' if (self.current_datatype == str) else '+'} {self._output[0]})', [other]+self._output[1]) if isinstance(other, int) or isinstance(other , float)  or isinstance(other, self.col_obj.table_obj._PlaceHolder) else (f'(%s || {self._output[0]})', [other if isinstance(other, str) else str(other)]+self._output[1])
        new_op.current_datatype = str if (isinstance(other, ColumnsOperation) and other.current_datatype == str) or (isinstance(other, Column) and other.datatype == str) or self.current_datatype == str or ( not isinstance(other, ColumnsOperation) and not isinstance(other,Column) and not isinstance(other, int) and not isinstance(other, float)) else self.current_datatype
        return new_op

    def __sub__(self, other):
        """
        Implement subtraction (`self - other`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance is used
        with the subtraction operator. It generates a SQL expression string
        and accumulates parameter values. The behavior depends on the type of
        ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both SQL fragments are
        combined with a subtraction operator, and the parameter lists are merged.
        - If ``other`` is a :class:`Column`, its name is used directly as the
        right-hand side of the subtraction.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used,
        and the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The right operand to
                subtract from this operation. Its type determines how the SQL
                expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.age`` and a constant value::

                op = users.age - 5
                # Resulting SQL: (users.age - %s), params: [5]

            For subtraction between two column expressions::

                op = users.salary - users.bonus
                # Resulting SQL: (users.salary - users.bonus), params: []
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} - {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} - {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} - %s)', self._output[1]+[other])
        return new_op

    def __rsub__(self, other):
        """
        Implement right-side subtraction (`other - self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of a ``-`` operator. It generates a SQL expression
        string and accumulates parameter values. The behavior depends on the
        type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``-`` operator, and their parameter lists are merged.
        - If ``other`` is a :class:`Column`, its name is used directly as the
        left operand.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is collected.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The left operand
                from which this operation will be subtracted. Its type determines
                how the SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.age`` and a constant value::

                # Right subtraction: 100 - users.age
                op = 100 - users.age
                # The __rsub__ is called for 100 - users.age

                # Resulting SQL: (%s - users.age)
                # Parameter: [100]

            With another column::

                op = users.max_age - users.age
                # __rsub__ may be called if users.max_age is on the left

            For string columns, subtraction is not typically used, but the
            operator is supported for numeric expressions.
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({other._output[0]} - {self._output[0]})', other._output[1] + self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} - {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s - {self._output[0]})', [other]+self._output[1])
        return new_op

    def __mul__(self, other):
        """
        Implement multiplication (`self * other`) for column operations.

        This method generates a SQL multiplication expression between the current
        column operation and the provided operand. It is called when a
        :class:`ColumnsOperation` instance is multiplied by another value.
        The behavior depends on the type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``*`` operator.
        - If ``other`` is a :class:`Column`, its fully qualified name is used
        directly as the right operand.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The right operand to
                multiply with this operation.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to represent the multiplication expression.

        Example:
            Multiplying a numeric column by a constant::

                from ormophine.Mysql import DataTypes, TableStructure, Driver

                # Assume a 'products' table with a 'price' column (numeric)
                # and we want to apply a 10% discount
                discounted = products.price * 0.9
                # discounted._output -> ('(products.price * %s)', [0.9])

            Multiplying two columns::

                total = products.quantity * products.price
                # total._output -> ('(products.quantity * products.price)', [])
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} * {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} * {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} * %s)', self._output[1]+[other])
        return new_op

    def __rmul__(self, other):
        """
        Implement right-side multiplication (`other * self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of a ``*`` operator. It generates a SQL expression
        string and accumulates parameter values. The behavior depends on the
        type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``*`` operator.
        - If ``other`` is a :class:`Column`, its name is used directly.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is collected.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The left operand
                to be multiplied with this operation. Its type determines how the
                SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.age``::

                op = 2 * users.age
                # The __rmul__ is called for 2 * users.age
                # Resulting SQL: (%s * users.age)

            For column expressions::

                op = users.income * 0.1
                # __rmul__ is called for 0.1 * users.income
                # Resulting SQL: (%s * users.income)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({other._output[0]} * {self._output[0]})', other._output[1] + self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} * {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s * {self._output[0]})', [other]+self._output[1])
        return new_op

    def __pow__(self, other):
        """
        Implement the exponentiation (power) operator (`self ** other`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance is used
        with the ``**`` operator. It generates a SQL ``POW()`` expression and
        accumulates parameter values. The behavior depends on the type of
        ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        as ``POW(left_expression, right_expression)``.
        - If ``other`` is a :class:`Column`, its name is used as the exponent,
        resulting in ``POW(expression, column_name)``.
        - If ``other`` is a numeric value (``int`` or ``float``), a placeholder
        ``%s`` is used for the value, and the parameter is collected.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The exponent (right
                operand). Its type determines how the SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the exponentiation operation. This enables method
            chaining.

        Example:
            Assuming a numeric column ``users.salary`` and a constant value::

                # Exponentiation with a constant
                op = users.salary ** 2
                # Resulting SQL: POW(users.salary, %s) with param [2]

                # Exponentiation with another column
                op = users.salary ** users.experience_years
                # Resulting SQL: POW(users.salary, users.experience_years)

                # Chaining with other operations
                op = (users.salary ** 2) + users.bonus
                # Resulting SQL: (POW(users.salary, %s) + users.bonus)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'(POW({self._output[0]} , {other._output[0]}))', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'(POW({self._output[0]} , {other.name}))', self._output[1]) if isinstance(other, Column) else (f'(POW({self._output[0]} , %s))', self._output[1]+[other])
        return new_op

    def __rpow__(self, other):
        """
        Implement right-side exponentiation (`other ** self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of the ``**`` operator. It generates a SQL expression
        using the ``POW()`` function and accumulates parameter values. The
        behavior depends on the type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using ``POW(other_expression, self_expression)``.
        - If ``other`` is a :class:`Column`, its name is used as the base, and
        the exponent is the current operation's expression.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the base value, and the parameter is collected.
        - Any other type is treated as a literal value (converted to string) and
        used with a placeholder.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float, Any): The left operand
                (base) to be raised to the power of this operation (exponent).
                Its type determines how the SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.score``::

                # Right exponentiation with a constant
                op = 2 ** users.score
                # Resulting SQL: POW(%s, users.score) with parameter 2

                # With another column operation
                op = (users.age + 1) ** users.score
                # Resulting SQL: POW((users.age + 1), users.score)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'(POW({other._output[0]} , {self._output[0]}))', other._output[1] + self._output[1]) if isinstance(other, ColumnsOperation) else (f'(POW({other.name} , {self._output[0]}))', self._output[1]) if isinstance(other, Column) else (f'(POW(%s , {self._output[0]}))', [other]+self._output[1])
        return new_op

    def __truediv__(self, other):
        """
        Implement left-side division (`self / other`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance is divided
        by another value using the ``/`` operator. It generates a SQL expression
        string and accumulates parameter values. The behavior depends on the type
        of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using ``(left_expression / right_expression)``.
        - If ``other`` is a :class:`Column`, its name is used as the divisor, and
        the expression becomes ``(current_expression / column_name)``.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the divisor value, and the parameter is collected.
        - Any other type is treated as a literal value (converted to string) and
        used with a placeholder.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float, Any): The right operand
                (divisor) to divide this operation by. Its type determines how the
                SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.score``::

                # Division by a constant
                op = users.score / 2
                # Resulting SQL: (users.score / %s) with parameter 2

                # Division by another column
                op = users.score / users.max_score
                # Resulting SQL: (users.score / users.max_score)
        """
        new_op = ColumnsOperation(self.col_obj)
        other_dt = other.current_datatype if isinstance(other, ColumnsOperation) else other.datatype if isinstance(other, Column) else int if isinstance(other, bool) or isinstance(other, int) else float if isinstance(other, float) else str if isinstance(other, str) else bytes if isinstance(other, bytes) else None
        use_div = (self.current_datatype is int) and (other_dt is int)
        op_symbol = 'DIV' if use_div else '/'
        new_op._output = ( (f'({self._output[0]} {op_symbol} {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} {op_symbol} {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} {op_symbol} %s)', self._output[1] + [other]) )
        new_op.current_datatype = int if use_div else float
        return new_op

    def __rtruediv__(self, other):
        """
        Implement right-side division (`other / self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of the ``/`` operator. It generates a SQL expression
        using division and accumulates parameter values. The behavior depends
        on the type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        as ``(other_expression / self_expression)``.
        - If ``other`` is a :class:`Column`, its name is used as the numerator,
        and the current operation's expression is the denominator.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the numerator value, and the parameter is collected.
        - Any other type is treated as a literal value and used with a placeholder.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float, Any): The left operand
                (numerator) to be divided by this operation (denominator).
                Its type determines how the SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.score``::

                # Right division with a constant
                op = 100 / users.score
                # Resulting SQL: (%s / users.score) with parameter 100

                # With another column operation
                op = (users.age + 1) / users.score
                # Resulting SQL: ((users.age + 1) / users.score)
        """
        new_op = ColumnsOperation(self.col_obj)
        other_dt = other.current_datatype if isinstance(other, ColumnsOperation) else other.datatype if isinstance(other, Column) else int if isinstance(other, bool) or isinstance(other, int) else float if isinstance(other, float) else str if isinstance(other, str) else bytes if isinstance(other, bytes) else None
        use_div = (other_dt is int) and (self.current_datatype is int)
        op_symbol = 'DIV' if use_div else '/'
        new_op._output = ( (f'({other._output[0]} {op_symbol} {self._output[0]})', other._output[1] + self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} {op_symbol} {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s {op_symbol} {self._output[0]})', [other] + self._output[1]) )
        new_op.current_datatype = int if use_div else float
        return new_op

    def __mod__(self, other):
        """
        Implement the modulo operation (`self % other`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the left side of the ``%`` operator. It generates a SQL expression
        using the modulo operator and accumulates parameter values. The behavior
        depends on the type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using the SQL modulo operator ``%``.
        - If ``other`` is a :class:`Column`, its name is used directly as the
        right operand.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is collected.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float): The right operand
                for the modulo operation. Its type determines how the SQL
                expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Modulo operation with a constant
                op = users.age % 10
                # Resulting SQL: (users.age % %s) with parameter 10

                # With another column operation
                op = users.age % (users.birth_year + 5)
                # Resulting SQL: (users.age % (users.birth_year + 5))
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} % {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} % {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} % %s)', self._output[1]+[other])
        return new_op

    def __rmod__(self, other):
        """
        Implement right-side modulo (`other % self`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance appears
        on the right side of the ``%`` operator. It generates a SQL expression
        using the modulo operator and accumulates parameter values. The behavior
        depends on the type of ``other``:

        - If ``other`` is a :class:`ColumnsOperation`, both sides are combined
        using ``(other_expression % self_expression)``.
        - If ``other`` is a :class:`Column`, its name is used as the left operand,
        and the current operation's expression is the right operand.
        - If ``other`` is an ``int`` or ``float``, a placeholder ``%s`` is used
        for the value, and the parameter is collected.
        - Any other type is treated as a literal value (converted to string) and
        used with a placeholder.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            other (ColumnsOperation, Column, int, float, Any): The left operand
                to be divided by this operation (modulo). Its type determines how
                the SQL expression is constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the new operation. This enables method chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Right modulo with a constant
                op = 10 % users.age
                # Resulting SQL: (%s % users.age) with parameter 10

                # With another column operation
                op = (users.age + 5) % users.age
                # Resulting SQL: ((users.age + 5) % users.age)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({other._output[0]} % {self._output[0]})', other._output[1] + self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} % {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s % {self._output[0]})', [other]+self._output[1])
        return new_op

    def __getitem__(self, key: slice):
        """
        Just like python string slicing , implement slicing (`self[start:stop]`) for string column operations.

        This method allows Python-like slicing syntax on :class:`ColumnsOperation`
        instances representing string columns. It generates a SQL ``SUBSTRING()``
        expression that extracts a substring from the column value based on the
        provided slice indices. The behavior mimics Python's string slicing,
        supporting positive and negative indices, as well as ``None`` for start or stop.

        The resulting SQL uses the ``SUBSTRING()`` function with appropriate start
        position and length calculations. For negative indices, the length of the
        string (``LENGTH()``) is used in the SQL expression.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self``, enabling chaining of operations.

        Note:
            This method is intended for use with string columns (``str`` datatype).
            Using it on numeric or other column types will produce invalid SQL.

        Args:
            key (slice): A Python slice object defining the substring range.
                The ``start`` and ``stop`` attributes can be ``None``, positive,
                or negative integers. A step is not supported (only start and stop).

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the substring operation. This enables method chaining.

        Raises:
            None: This method does not raise exceptions directly, but using it
            with non-string columns will result in SQL errors during execution.

        Example:
            Assuming a string column ``users.name``::

                # Get first 5 characters
                op = users.name[:5]
                # SQL: (SUBSTRING(users.name, 1, 5))

                # Get from position 3 to the end
                op = users.name[2:]
                # SQL: (SUBSTRING(users.name, 3, LENGTH(users.name)))

                # Get last 3 characters (negative indexing)
                op = users.name[-3:]
                # SQL: (SUBSTRING(users.name, LENGTH(users.name) - 2, LENGTH(users.name)))

                # Get substring from 3rd character to 2 before the end
                op = users.name[2:-2]
                # SQL: (SUBSTRING(users.name, 3, LENGTH(users.name) - 4))
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op.current_datatype = str
        if self._output:
            if key.start == None and key.stop ==  None:
                new_op._output = (f'(SUBSTRING({self._output[0]} , 1 , LENGTH({self._output[0]}) + 1))', self._output[1] + self._output[1])   #
            elif key.start == None and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , 1 , LENGTH({self._output[0]}) - %s))', self._output[1] + self._output[1] + [abs(key.stop)])  #
            elif key.start == None and key.stop >= 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , 1 , %s))', self._output[1] + [key.stop])  #  
            elif key.start >= 0 and key.stop ==  None:
                new_op._output = (f'(SUBSTRING({self._output[0]} , %s , LENGTH({self._output[0]})))', self._output[1] + [key.start + 1] + self._output[1])  #   
            elif key.start < 0 and key.stop == None:
                new_op._output = (f'(SUBSTRING({self._output[0]} , LENGTH({self._output[0]}) - %s , LENGTH({self._output[0]})))', self._output[1] + self._output[1] + [abs(key.start) - 1] + self._output[1])  #
            elif key.start >= 0 and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , %s , LENGTH({self._output[0]}) - %s))', self._output[1] +  [key.start + 1] + self._output[1] + [abs(key.stop - key.start)])  #  
            elif key.start >= 0 and key.stop > 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , %s , %s))', self._output[1] + [key.start + 1, key.stop - key.start])  #
            elif key.start < 0 and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , LENGTH({self._output[0]}) - %s , %s))', self._output[1] + self._output[1] + [abs(key.start) - 1, key.stop - key.start])  #
            elif key.start < 0 and key.stop > 0:
                new_op._output = (f'(SUBSTRING({self._output[0]} , LENGTH({self._output[0]}) - %s ,  %s - (LENGTH({self._output[0]}) - %s)))', self._output[1] + self._output[1] + [abs(key.start) - 1, key.stop] + self._output[1] + [abs(key.start)])
        else:
            if key.start == None and key.stop ==  None:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , 1 , LENGTH({self.col_obj.name}) + 1))', [])   #
            elif key.start == None and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , 1 , LENGTH({self.col_obj.name}) - %s))', [abs(key.stop)])  #
            elif key.start == None and key.stop >= 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , 1 , %s))', [key.stop])  #  
            elif key.start >= 0 and key.stop ==  None:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , %s , LENGTH({self.col_obj.name})))', [key.start + 1])  #   
            elif key.start < 0 and key.stop == None:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , LENGTH({self.col_obj.name}) - %s , LENGTH({self.col_obj.name})))', [abs(key.start) - 1])  #
            elif key.start >= 0 and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , %s , LENGTH({self.col_obj.name}) - %s))', [key.start + 1, abs(key.stop - key.start)])  #  
            elif key.start >= 0 and key.stop > 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , %s , %s))', [key.start + 1, key.stop - key.start])  #
            elif key.start < 0 and key.stop < 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , LENGTH({self.col_obj.name}) - %s , %s)', [abs(key.start) - 1, key.stop - key.start])  #
            elif key.start < 0 and key.stop > 0:
                new_op._output = (f'(SUBSTRING({self.col_obj.name} , LENGTH({self.col_obj.name}) - %s ,  %s - (LENGTH({self.col_obj.name}) - %s)))', [abs(key.start) - 1, key.stop, abs(key.start)])
        return new_op

    def eq(self, value):
        """
        Create an equality comparison expression.

        This method generates a SQL equality operation between the current
        expression and the provided value. It updates the internal ``_output``
        tuple (SQL fragment and parameter list) and returns ``self`` to enable
        method chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                equality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NULL``.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the equality comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create an equality condition
                condition = users.age.eq(25)
                # condition._output[0] -> '(users.age = %s)'
                # condition._output[1] -> [25]

                # Chain with other operations
                condition = users.age.eq(users.id)  # Compare two columns
                # condition._output[0] -> '(users.age = users.id)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} = {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} = {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} IS NULL)', self._output[1]) if value is None else (f'({self._output[0]} = %s)', self._output[1] + [value])
        return new_op

    def __eq__(self, value):
        """
        Implement equality comparison (`self == value`) for column operations.

        This magic method is called when a :class:`ColumnsOperation` instance is
        compared with another value using the ``==`` operator. It generates a SQL
        equality expression and updates the internal ``_output`` tuple (SQL
        fragment and parameter list). The method returns ``self`` to enable
        method chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                equality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NULL``.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the equality comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create an equality condition using ==
                condition = users.age == 25
                # condition._output[0] -> '(users.age = %s)'
                # condition._output[1] -> [25]

                # Compare two columns
                condition = users.age == users.id
                # condition._output[0] -> '(users.age = users.id)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} = {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} = {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} IS NULL)', self._output[1]) if value is None else (f'({self._output[0]} = %s)', self._output[1] + [value])
        return new_op

    def ne(self, value):
        """
        Create a non‑equality (not equal) comparison expression.

        This method generates a SQL inequality operation between the current
        expression and the provided value. It updates the internal ``_output``
        tuple (SQL fragment and parameter list) and returns ``self`` to enable
        method chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``!=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                inequality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NOT NULL``.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the inequality comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create an inequality condition
                condition = users.age.ne(25)
                # condition._output[0] -> '(users.age != %s)'
                # condition._output[1] -> [25]

                # Chain with other operations
                condition = users.age.ne(users.id)  # Compare two columns
                # condition._output[0] -> '(users.age != users.id)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} != {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} != {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} IS NOT NULL)', self._output[1]) if value is None else (f'({self._output[0]} != %s)', self._output[1] + [value])
        return new_op

    def __ne__(self, value):
        """
        Implement the inequality operator (`!=`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance is compared
        with another value using the ``!=`` operator. It generates a SQL inequality
        expression and accumulates parameter values. The behavior depends on the
        type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``!=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                inequality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NOT NULL``.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the inequality operation. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create an inequality condition
                condition = users.age.__ne__(25)
                # Equivalent to: users.age != 25
                # condition._output[0] -> '(users.age != %s)'
                # condition._output[1] -> [25]

                # Compare two columns
                condition = users.age.__ne__(users.id)
                # condition._output[0] -> '(users.age != users.id)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} != {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} != {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} IS NOT NULL)', self._output[1]) if value is None else (f'({self._output[0]} != %s)', self._output[1] + [value])
        return new_op

    def gt(self, value):
        """
        Create a greater-than comparison expression.

        This method generates a SQL ``>`` operation between the current expression
        and the provided value. It updates the internal ``_output`` tuple (SQL
        fragment and parameter list) and returns ``self`` to enable method
        chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``>`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                comparison. Its type determines how the SQL expression and
                parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the greater-than comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a greater-than condition
                condition = users.age.gt(18)
                # condition._output[0] -> '(users.age > %s)'
                # condition._output[1] -> [18]

                # Chain with other operations
                condition = users.age.gt(users.min_age)  # Compare two columns
                # condition._output[0] -> '(users.age > users.min_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} > {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} > {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} > %s)', self._output[1] + [value])
        return new_op

    def __gt__(self, value):
        """
        Implement the greater-than comparison operator (`>`).

        This method is called when a :class:`ColumnsOperation` instance is compared
        with another value using the ``>`` operator. It generates a SQL expression
        string and accumulates parameter values. The behavior depends on the type
        of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``>`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                greater-than comparison. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a greater-than condition
                condition = users.age > 25
                # condition._output[0] -> '(users.age > %s)'
                # condition._output[1] -> [25]

                # Chain with other comparisons
                condition = (users.age > 18) & (users.age < 65)
                # Generates: ((users.age > %s) AND (users.age < %s))
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} > {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} > {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} > %s)', self._output[1] + [value])
        return new_op

    def lt(self, value):
        """
        Create a less-than comparison expression.

        This method generates a SQL less-than operation between the current
        expression and the provided value. It updates the internal ``_output``
        tuple (SQL fragment and parameter list) and returns ``self`` to enable
        method chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``<`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                less-than comparison. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the less-than comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a less-than condition
                condition = users.age.lt(25)
                # condition._output[0] -> '(users.age < %s)'
                # condition._output[1] -> [25]

                # Chain with other operations
                condition = users.age.lt(users.max_age)  # Compare two columns
                # condition._output[0] -> '(users.age < users.max_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} < {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} < {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} < %s)', self._output[1] + [value])
        return new_op

    def __lt__(self, value):
        """
        Implement the less-than operator (`<`) for column operations.

        This method is invoked when a :class:`ColumnsOperation` instance is used
        with the ``<`` operator (e.g., ``op < value``). It generates a SQL
        less-than expression and updates the internal ``_output`` tuple (SQL
        fragment and parameter list). The behavior depends on the type of
        ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``<`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                less-than comparison. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the less-than operation. This enables chaining.

        Example:
            Using the ``<`` operator with a numeric column::

                from ormophine.Mysql import Column

                # Assuming `users.age` is a Column instance
                condition = users.age < 25
                # condition._output[0] -> '(users.age < %s)'
                # condition._output[1] -> [25]

                # Compare two columns
                condition = users.age < users.max_age
                # condition._output[0] -> '(users.age < users.max_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} < {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} < {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} < %s)', self._output[1] + [value])
        return new_op

    def ge(self, value):
        """
        Create a greater-than-or-equal-to comparison expression.

        This method generates a SQL greater-than-or-equal operation between the
        current expression and the provided value. It updates the internal
        ``_output`` tuple (SQL fragment and parameter list) and returns ``self``
        to enable method chaining. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``>=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                greater-than-or-equal comparison. Its type determines how the
                SQL expression and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the comparison. This enables chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a greater-than-or-equal condition
                condition = users.age.ge(18)
                # condition._output[0] -> '(users.age >= %s)'
                # condition._output[1] -> [18]

                # Chain with other operations
                condition = users.age.ge(users.min_age)  # Compare two columns
                # condition._output[0] -> '(users.age >= users.min_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} >= {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} >= {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} >= %s)', self._output[1] + [value])
        return new_op

    def __ge__(self, value):
        """
        Implement greater-than-or-equal comparison (`self >= value`) for column operations.

        This method is called when a :class:`ColumnsOperation` instance is compared
        using the ``>=`` operator. It generates a SQL expression string and
        accumulates parameter values. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``>=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                comparison. Its type determines how the SQL expression and
                parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the greater-than-or-equal comparison. This enables
            chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a condition using the >= operator
                condition = users.age >= 18
                # condition._output[0] -> '(users.age >= %s)'
                # condition._output[1] -> [18]

                # Compare two columns
                condition = users.age >= users.min_age
                # condition._output[0] -> '(users.age >= users.min_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} >= {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} >= {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} >= %s)', self._output[1] + [value])
        return new_op

    def le(self, value):
        """
        Create a less-than-or-equal-to comparison expression.

        This method generates a SQL ``<=`` operation between the current expression
        and the provided value. It updates the internal ``_output`` tuple (SQL
        fragment and parameter list) and returns ``self`` to enable method chaining.
        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using the ``<=`` operator, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                less-than-or-equal-to comparison. Its type determines how the SQL
                expression and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the less-than-or-equal-to comparison. This enables
            chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a less-than-or-equal-to condition
                condition = users.age.le(25)
                # condition._output[0] -> '(users.age <= %s)'
                # condition._output[1] -> [25]

                # Chain with other operations
                condition = users.age.le(users.max_age)  # Compare two columns
                # condition._output[0] -> '(users.age <= users.max_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} <= {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} <= {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} <= %s)', self._output[1] + [value])
        return new_op

    def __le__(self, value):
        """
        Implement less-than-or-equal comparison (`self <= value`).

        This special method is called when a :class:`ColumnsOperation` instance
        is compared with another value using the ``<=`` operator. It generates a
        SQL expression using the ``<=`` operator and accumulates parameter values.
        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        using ``<=``, and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to allow chaining.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                less-than-or-equal comparison. Its type determines how the SQL
                expression and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the less-than-or-equal comparison. This enables
            method chaining.

        Example:
            Assuming a numeric column ``users.age``::

                # Create a <= condition using the operator
                condition = users.age <= 25
                # condition._output[0] -> '(users.age <= %s)'
                # condition._output[1] -> [25]

                # Compare two columns
                condition = users.age <= users.max_age
                # condition._output[0] -> '(users.age <= users.max_age)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} <= {value._output[0]})', self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} <= {value.name})', self._output[1] if isinstance(self._output[1], list) else [self._output[1]]) if isinstance(value, Column) else (f'({self._output[0]} <= %s)', self._output[1] + [value])
        return new_op

    def __and__(self, value):
        """
        Implement the bitwise AND operator (`&`) as a logical AND for SQL conditions.

        This method is called when a :class:`ColumnsOperation` instance is used with
        the ``&`` operator. It generates a SQL expression combining the current
        operation's SQL fragment and the provided value's SQL fragment with an
        ``AND`` between them. Both parameter lists are merged, and the internal
        ``_output`` tuple is updated accordingly.

        The method returns ``self`` to allow chaining of conditions, making it
        convenient to build complex WHERE clauses.

        Args:
            value (ColumnsOperation): The right-hand side operation to combine
                with the current operation using ``AND``. Must be a
                :class:`ColumnsOperation` instance.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the combined condition. This enables chaining.

        Raises:
            AttributeError: If ``value`` is not a :class:`ColumnsOperation` (though
                the implementation does not explicitly check for this, the intended
                usage is with another operation).

        Example:
            Building a complex WHERE condition using ``&`` to combine conditions::

                from ormophine.Mysql import Column

                # Assume users is a Table instance with columns: id, name, age
                condition = (users.age > 18) & (users.name.startswith('A'))
                # condition._output[0] -> '((users.age > %s) AND (users.name like %s || '%%'))'
                # condition._output[1] -> [18, 'A']

                # Use in a query
                rows = users.get_row(which_columns=[users.id, users.name],
                                    where=condition)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} AND {value._output[0]})', self._output[1] + value._output[1])
        return new_op

    def __or__(self, value):
        """
        Implement the bitwise OR operator (`|`) as a logical OR for SQL conditions.

        This method is called when a :class:`ColumnsOperation` instance is used with
        the ``|`` operator. It generates a SQL expression combining the current
        operation's SQL fragment and the provided value's SQL fragment with an
        ``OR`` between them. Both parameter lists are merged, and the internal
        ``_output`` tuple is updated accordingly.

        The method returns ``self`` to allow chaining of conditions, making it
        convenient to build complex WHERE clauses.

        Args:
            value (ColumnsOperation): The right-hand side operation to combine
                with the current operation using ``OR``. Must be a
                :class:`ColumnsOperation` instance.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the combined condition. This enables chaining.

        Raises:
            AttributeError: If ``value`` is not a :class:`ColumnsOperation` (though
                the implementation does not explicitly check for this, the intended
                usage is with another operation).

        Example:
            Building a complex WHERE condition using ``|`` to combine conditions::

                from ormophine.Mysql import Column

                # Assume users is a Table instance with columns: id, name, age
                condition = (users.age < 18) | (users.age > 65)
                # condition._output[0] -> '((users.age < %s) OR (users.age > %s))'
                # condition._output[1] -> [18, 65]

                # Use in a query
                rows = users.get_row(which_columns=[users.id, users.name],
                                    where=condition)
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} OR {value._output[0]})', self._output[1] + value._output[1])
        return new_op

    def like(self, value):
        """
        Create a SQL LIKE pattern-matching comparison.

        This method generates a SQL `LIKE` expression between the current column
        operation and a pattern value. The pattern can be a literal string,
        another column, or a complex operation. The method updates the internal
        ``_output`` tuple (SQL fragment and parameter list) and returns ``self``
        to enable method chaining.

        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined
        with the `LIKE` operator, and their parameter lists are merged.
        - If ``value`` is a :class:`Column`, its name is used directly on the
        right side, and no additional parameters are added.
        - For any other type (typically a string), the value is treated as a
        literal pattern, a placeholder ``%s`` is used, and the value is added
        to the parameter list after converting to a string.

        Note that the `LIKE` operator in MySQL performs pattern matching with
        ``%`` and ``_`` wildcards. For exact string matching, consider using
        :meth:`eq`.

        Args:
            value (ColumnsOperation, Column, str): The pattern to match against.
                If a string, it will be used as a literal pattern with placeholder.
                If a Column, its name is used directly. If another operation,
                the combined SQL expression is used.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the LIKE comparison. This enables chaining.

        Example:
            Using the `like` method to find users whose names start with 'A'::

                # Assuming users is a Table instance with a 'name' Column
                condition = users.name.like('A%')
                # condition._output[0] -> '(users.name like %s)'
                # condition._output[1] -> ['A%']

                # Combining with another condition
                condition = users.name.like(users.pattern_column)
                # condition._output[0] -> '(users.name like users.pattern_column)'

            For more complex patterns, you can use :meth:`startswith`, :meth:`endswith`,
            or :meth:`contains` which build the appropriate LIKE patterns with
            wildcards automatically.
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"({self._output[0]} like {value._output[0]})", (self._output[1] + value._output[1]) if self._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self._output[0]} like {value.name})', self._output[1]) if isinstance(value , Column) else (f'({self._output[0]} like %s)', self._output[1] + [f'{value}'])
        return new_op

    def startswith(self, prefix):
        """
        Just like python startswith(), create a SQL condition that checks if the current expression starts with a given prefix.

        This method generates a ``LIKE`` condition with the prefix followed by a wildcard
        (``'%%'``), effectively testing whether the expression's value begins with the
        specified prefix. It updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining. The behavior
        depends on the type of ``prefix``:

        - If ``prefix`` is a :class:`ColumnsOperation`, both sides are combined using
        the ``LIKE`` operator with string concatenation (``|| '%%'``), and parameters
        are merged.
        - If ``prefix`` is a :class:`Column`, its name is used directly on the right
        side, and the wildcard is concatenated using ``|| '%%'``.
        - For any other type (e.g., a string literal), a placeholder ``%s`` is used
        for the value, and the wildcard is appended in the SQL fragment, with the
        parameter added to the list.

        Args:
            prefix (ColumnsOperation, Column, Any): The prefix to test against. Its
                type determines how the SQL expression and parameters are constructed.
                If a literal value is provided, it is automatically converted to a
                string and used as a parameter.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the ``STARTSWITH`` condition. This enables chaining.

        Example:
            Assuming a string column ``users.name``::

                # Create a condition for names starting with 'A'
                condition = users.name.startswith('A')
                # condition._output[0] -> '(users.name like %s || '%%')'
                # condition._output[1] -> ['A']

                # Use with a Column as prefix
                prefix_col = users.prefix_column
                condition = users.name.startswith(prefix_col)
                # condition._output[0] -> '(users.name like users.prefix_column || '%%')'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"({self._output[0]} like {prefix._output[0]} || '%%)'", (self._output[1] + prefix._output[1]) if self._output[0] else prefix._output[1]) if isinstance(prefix, ColumnsOperation) else (f"({self._output[0]} like {prefix.name} || '%%')", self._output[1]) if isinstance(prefix , Column) else (f"({self._output[0]} like %s || '%%')", self._output[1] + [f'{prefix}'])
        return new_op

    def endswith(self, suffix):
        """
        Just like python endswith(), create a SQL condition that checks if the current expression ends with a suffix.

        This method generates a SQL ``LIKE`` expression using the pattern
        ``'%%' || suffix``, which matches strings that end with the specified suffix.
        The operator used is ``LIKE`` with concatenation (``||``) to combine the
        wildcard prefix with the suffix. The method updates the internal ``_output``
        tuple (SQL fragment and parameter list) and returns ``self`` to enable
        chaining.

        The behavior depends on the type of ``suffix``:

        - If ``suffix`` is a :class:`ColumnsOperation`, both sides are combined
        using the pattern ``'%%' || suffix_expression``, and parameters are merged.
        - If ``suffix`` is a :class:`Column`, its name is used directly, and no
        additional parameters are added.
        - If ``suffix`` is a literal value (e.g., ``str``), a placeholder ``%s`` is
        used for the suffix, and the value is added to the parameter list.

        Args:
            suffix (ColumnsOperation, Column, str): The suffix to check for at the end
                of the current expression. Its type determines how the SQL pattern
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the ``LIKE`` condition. This enables chaining.

        Example:
            Assuming a string column ``users.email``::

                # Check if email ends with '@example.com'
                condition = users.email.endswith('@example.com')
                # condition._output[0] -> "(users.email like '%%' || %s)"
                # condition._output[1] -> ['@example.com']

                # Using a column as the suffix
                condition = users.email.endswith(users.domain)
                # condition._output[0] -> "(users.email like '%%' || users.domain)"

                # Chain with other conditions
                condition = (users.email.endswith('@gmail.com') & users.age >= 18)
                # condition._output[0] -> "((users.email like '%%' || %s) AND (users.age >= %s))"
                # condition._output[1] -> ['@gmail.com', 18]
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"({self._output[0]} like '%%' || {suffix._output[0]})", (self._output[1] + suffix._output[1]) if self._output[0] else suffix._output[1]) if isinstance(suffix, ColumnsOperation) else (f"({self._output[0]} like '%%' || {suffix.name})", self._output[1]) if isinstance(suffix , Column) else (f"({self._output[0]} like '%%' || %s)", self._output[1] + [f'{suffix}'])
        return new_op

    def contains(self, value):
        """
        Create a SQL LIKE condition to check if the expression contains a substring.

        This method generates a SQL ``LIKE`` expression with the pattern ``'%%' || value || '%%'``,
        effectively checking whether the current expression contains the specified substring.
        The method updates the internal ``_output`` tuple (SQL fragment and parameter list)
        and returns ``self`` to enable method chaining. The behavior depends on the type of
        ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, both sides are combined using
        the ``LIKE`` operator with the pattern ``'%%' || value_expression || '%%'``,
        and parameters are merged.
        - If ``value`` is a :class:`Column`, its name is used directly in the pattern,
        and no additional parameters are added.
        - For any other type (e.g., int, float, str), the value is converted to a string
        and used as a parameter with the ``'%%' || %s || '%%'`` pattern.

        Args:
            value (ColumnsOperation, Column, Any): The substring to search for. Its
                type determines how the SQL expression and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the ``LIKE`` condition. This enables chaining.

        Example:
            Assuming a string column ``users.name``::

                # Check if name contains 'smith'
                condition = users.name.contains('smith')
                # condition._output[0] -> '(users.name like '%%' || %s || '%%')'
                # condition._output[1] -> ['smith']

                # Chain with other operations
                condition = users.name.contains(users.partial_name)
                # condition._output[0] -> '(users.name like '%%' || users.partial_name || '%%')'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"({self._output[0]} like '%%' || {value._output[0]} || '%%')", (self._output[1] + value._output[1]) if self._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f"({self._output[0]} like '%%' || {value.name} || '%%')", self._output[1]) if isinstance(value , Column) else (f"({self._output[0]} like '%%' || %s || '%%')", self._output[1] + [f'{value}'])
        return new_op

    def add_end(self, content):
        """
        Concatenate content to the end of the current expression.

        This method generates a SQL string concatenation operation using the ``||``
        operator. It appends the provided ``content`` to the right side of the
        current expression. The behavior depends on the type of ``content``:

        - If ``content`` is a :class:`ColumnsOperation`, both SQL fragments are
        combined with ``||``, and their parameter lists are merged.
        - If ``content`` is a :class:`Column`, its name is used directly, and no
        additional parameters are added.
        - For any other type (e.g., str, int), a placeholder ``%s`` is used, and
        the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        This method is intended for string columns only. Using it on numeric
        columns may result in unintended SQL behavior (the driver's SQL mode
        must have ``PIPES_AS_CONCAT`` enabled for ``||`` to perform concatenation).

        Args:
            content (ColumnsOperation, Column, Any): The content to append to
                the current expression. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the concatenation. This enables chaining.

        Example:
            Assuming a string column ``users.full_name``::

                # Append a space and a last name
                op = users.full_name.add_end(' ').add_end('Smith')
                # op._output[0] -> '((users.full_name || %s) || %s)'
                # op._output[1] -> [' ', 'Smith']

                # Using with another ColumnsOperation
                suffix = users.last_name
                op = users.first_name.add_end(suffix)
                # op._output[0] -> '(users.first_name || users.last_name)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} || {content._output[0]})', self._output[1]+content._output[1] if self._output[0] else content._output[1]) if isinstance(content, ColumnsOperation) else (f'({self._output[0]} || {content.name})', self._output[1] if self._output[0] else []) if isinstance(content, Column) else (f'({self._output[0]} || %s)', self._output[1]+[content] if self._output[0] else [content])
        new_op.current_datatype = str
        return new_op

    def add_first(self, content):
        """
        Concatenate content to the beginning of the current expression.

        This method generates a SQL string concatenation operation using the ``||``
        operator. It prepends the provided ``content`` to the left side of the
        current expression. The behavior depends on the type of ``content``:

        - If ``content`` is a :class:`ColumnsOperation`, both SQL fragments are
        combined with ``||``, and their parameter lists are merged.
        - If ``content`` is a :class:`Column`, its name is used directly, and no
        additional parameters are added.
        - For any other type (e.g., str, int), a placeholder ``%s`` is used, and
        the value is added to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        This method is intended for string columns only. Using it on numeric
        columns may result in unintended SQL behavior (the driver's SQL mode
        must have ``PIPES_AS_CONCAT`` enabled for ``||`` to perform concatenation).

        Args:
            content (ColumnsOperation, Column, Any): The content to prepend to
                the current expression. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the concatenation. This enables chaining.

        Example:
            Assuming a string column ``users.last_name``::

                # Prepend a first name and a space
                op = users.last_name.add_first('John ').add_first(' ')
                # op._output[0] -> '((%s || users.last_name) || %s)'
                # op._output[1] -> ['John ', ' ']

                # Using with another ColumnsOperation
                prefix = users.title
                op = users.last_name.add_first(prefix)
                # op._output[0] -> '(users.title || users.last_name)'
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({content._output[0]} || {self._output[0]})', content._output[1]+self._output[1] if self._output[0] else content._output[1]) if isinstance(content, ColumnsOperation) else (f'({content.name} || {self._output[0]})', self._output[1] if self._output[0] else []) if isinstance(content, Column) else (f'(%s || {self._output[0]})', [content]+self._output[1] if self._output[0] else [content])
        new_op.current_datatype = str
        return new_op

    def replace(self, old: str, new: str):
        """
        Generate a SQL REPLACE expression to substitute occurrences of a substring.

        This method creates a SQL expression using the ``REPLACE()`` function, which
        replaces all occurrences of a specified substring with a new substring in
        the current column or operation expression. The replacement is applied to
        the string value represented by the current operation.

        If the current operation already has an expression stored in ``_output``,
        that expression is used as the target. Otherwise, the original column name
        (``col_obj.name``) is used. Two placeholders (``%s``) are added for the
        old and new strings, and the parameters are appended to the parameter list.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        Args:
            old (str): The substring to be replaced.
            new (str): The substring to replace with.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the REPLACE operation. This enables chaining.

        Example:
            Assuming a string column ``users.bio``::

                # Replace 'old' with 'new' in the bio column
                op = users.bio.replace('old', 'new')
                # op._output[0] -> 'REPLACE(users.bio , %s , %s)'
                # op._output[1] -> ['old', 'new']

                # Chain with other operations
                op = users.bio.upper().replace('OLD', 'NEW')
                # op._output[0] -> 'REPLACE(UPPER(users.bio) , %s , %s)'
                # op._output[1] -> ['OLD', 'NEW']
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'(REPLACE({self._output[0]} , %s , %s))', self._output[1] + [old, new]) if self._output[0] else (f'(REPLACE({self.col_obj.name} , %s , %s))', [old, new])
        new_op.current_datatype = str
        return new_op

    def upper(self):
        """
        Just like python upper(), apply the SQL UPPER function to the current expression.

        This method generates a SQL expression that converts the current column
        or operation result to uppercase. It updates the internal ``_output``
        tuple (SQL fragment and parameter list) and returns ``self`` to enable
        method chaining. If the current expression is already an operation (i.e.,
        ``self._output`` is not empty), the UPPER function is applied to the
        existing SQL fragment; otherwise, it is applied to the original column
        name stored in ``self.col_obj``.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the UPPER transformation. This enables chaining.

        Example:
            Assuming a string column ``users.username``::

                # Convert username to uppercase in a condition
                condition = users.username.upper().eq('ADMIN')
                # condition._output[0] -> 'UPPER(users.username) = %s'
                # condition._output[1] -> ['ADMIN']

                # Chain with other operations
                result = users.username.upper().startswith('A')
                # result._output[0] -> "UPPER(users.username) like %s || '%%'"
                # result._output[1] -> ['A']
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'(UPPER({self._output[0]}))', self._output[1]) if self._output[0] else (f'(UPPER({self.col_obj.name}))', [])
        new_op.current_datatype = str
        return new_op

    def lower(self):
        """
        Just like python lower(), convert the current expression to lowercase using the SQL ``LOWER()`` function.

        This method generates a SQL fragment that wraps the current expression
        (or the column name if no operation has been applied yet) with the
        ``LOWER()`` function. It updates the internal ``_output`` tuple (SQL
        fragment and parameter list) and returns ``self`` to enable chaining.

        If the current operation already has an expression (e.g., after arithmetic
        or string operations), that expression is wrapped. Otherwise, the raw
        column name of the associated :class:`Column` is used.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the ``LOWER()`` transformation. This enables chaining.

        Example:
            Assuming a string column ``users.name``::

                # Convert the name to lowercase
                op = users.name.lower()
                # op._output[0] -> 'LOWER(users.name)'
                # op._output[1] -> []

                # Chain with other operations
                op = (users.first_name + ' ' + users.last_name).lower()
                # op._output[0] -> 'LOWER((users.first_name || %s || users.last_name))'
                # op._output[1] -> [' ']
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'(LOWER({self._output[0]}))', self._output[1]) if self._output[0] else (f'(LOWER({self.col_obj.name}))', [])
        new_op.current_datatype = str
        return new_op

    def strip(self, chars: str = ' '):
        """
        Just like python strip(), remove leading and trailing characters from the current expression.

        This method generates a SQL ``TRIM(BOTH ... FROM ...)`` expression that
        removes all occurrences of the specified ``chars`` from both ends of the
        current column or operation. If the current operation already contains
        a SQL fragment (i.e., ``_output`` is not empty), the ``TRIM`` is applied
        to that fragment; otherwise, it is applied directly to the associated
        column name.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        Args:
            chars (str, optional): A string containing the characters to remove
                from both ends. Defaults to a single space (``' '``).

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the trimming operation. This enables chaining.

        Example:
            Assuming a string column ``users.full_name``::

                # Remove leading/trailing spaces
                op = users.full_name.strip()
                # op._output[0] -> "TRIM(BOTH ' ' FROM users.full_name)"

                # Remove specific characters
                op = users.full_name.strip('_')
                # op._output[0] -> "TRIM(BOTH '_' FROM users.full_name)"

                # Chain with other operations
                op = (users.first_name + ' ' + users.last_name).strip()
                # op._output[0] -> "TRIM(BOTH ' ' FROM (users.first_name || ' ' || users.last_name))"
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"(TRIM(BOTH '{chars}' FROM {self._output[0]}))", self._output[1]) if self._output[0] else (f"(TRIM(BOTH '{chars}' FROM {self.col_obj.name}))", [])
        new_op.current_datatype = str
        return new_op

    def lstrip(self, chars: str = ' '):
        """
        Just like python lstrip(), generate a SQL expression that trims leading characters from a string.

        This method applies the SQL ``TRIM(LEADING ... FROM ...)`` function to the
        current string expression, removing all leading occurrences of the specified
        characters. If the current operation already has a SQL expression (i.e.,
        ``self._output`` is not empty), the trimming is applied to that expression.
        Otherwise, it is applied to the original column name stored in
        ``self.col_obj.name``.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        Args:
            chars (str, optional): A string of characters to remove from the
                leading end of the expression. Defaults to a single space ``' '``.
                If multiple characters are provided, each is treated as a separate
                character to be stripped.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the left‑trim operation. This enables chaining.

        Example:
            Assuming a string column ``users.name`` with leading whitespace::

                # Remove leading spaces from the column
                op = users.name.lstrip()
                # op._output[0] -> "TRIM(LEADING ' ' FROM users.name)"
                # op._output[1] -> []

                # Remove leading '@' characters
                op = users.username.lstrip('@')
                # op._output[0] -> "TRIM(LEADING '@' FROM users.username)"

                # Chain with other operations
                op = users.name.upper().lstrip()
                # op._output[0] -> "TRIM(LEADING ' ' FROM UPPER(users.name))"
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"(TRIM(LEADING '{chars}' FROM {self._output[0]}))", self._output[1]) if self._output[0] else (f"(TRIM(LEADING '{chars}' FROM {self.col_obj.name}))", [])
        new_op.current_datatype = str
        return new_op

    def rstrip(self, chars: str = ' '):
        """
        Just like python rstrip(), generate a SQL expression that trims trailing characters from a string.

        This method applies the SQL ``TRIM(TRAILING ... FROM ...)`` function to the
        current string expression, removing all trailing occurrences of the specified
        characters. If the current operation already has a SQL expression (i.e.,
        ``self._output`` is not empty), the trimming is applied to that expression.
        Otherwise, it is applied to the original column name stored in
        ``self.col_obj.name``.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        Args:
            chars (str, optional): A string of characters to remove from the
                trailing end of the expression. Defaults to a single space ``' '``.
                If multiple characters are provided, each is treated as a separate
                character to be stripped.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the right‑trim operation. This enables chaining.

        Example:
            Assuming a string column ``users.name`` with trailing whitespace::

                # Remove trailing spaces from the column
                op = users.name.rstrip()
                # op._output[0] -> "TRIM(TRAILING ' ' FROM users.name)"
                # op._output[1] -> []

                # Remove trailing '@' characters
                op = users.username.rstrip('@')
                # op._output[0] -> "TRIM(TRAILING '@' FROM users.username)"

                # Chain with other operations
                op = users.name.lower().rstrip()
                # op._output[0] -> "TRIM(TRAILING ' ' FROM LOWER(users.name))"
        """
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f"(TRIM(TRAILING '{chars}' FROM {self._output[0]}))", self._output[1]) if self._output[0] else (f"(TRIM(TRAILING '{chars}' FROM {self.col_obj.name}))", [])
        new_op.current_datatype = str
        return new_op

    def In(self, column: 'Ormophine.Mysql.Column|Ormophine.Mysql.ColumnsOperation' = None, where: 'Ormophine.Mysql.ColumnsOperation' = None, data_list: list = None):
        """Build an SQL ``IN`` clause for the current column expression.

        This method supports two distinct modes for generating an ``IN`` clause:

        * **Literal list mode**: When ``data_list`` is provided, generates a
        parameterised ``IN (%s, %s, ...)`` clause using the literal values.
        For backward compatibility, if a list of plain values is passed as
        the first positional argument (``column``), it is automatically
        treated as ``data_list``.
        * **Subquery mode**: When ``column`` is provided as a single
        :class:`Column` or :class:`Ormophine.Mysql.ColumnsOperation`, builds an
        ``IN (SELECT ...)`` subquery. The table name is extracted from the
        provided column object, and an optional ``where`` condition can be
        applied inside the subquery — handled identically to
        :meth:`Table.get_row`.

        The result is stored in the instance's ``_output`` attribute as a tuple
        ``(sql_string, parameters)``, and the instance is returned to allow
        chaining.

        Args:
            column: A single :class:`Column` or :class:`Ormophine.Mysql.ColumnsOperation`
                to use in the ``SELECT`` clause of the subquery. The table name
                is determined from this object. Do not pass a list of columns;
                if you need multiple conditions, chain them using ``&`` or ``|``.
                If a list of literals is passed, it is treated as ``data_list``.
            where: An optional :class:`Ormophine.Mysql.ColumnsOperation` (or
                :class:`Column` for boolean columns) representing the ``WHERE``
                condition for the subquery. Defaults to ``None``.
            data_list: A list of literal values for a direct ``IN`` clause.
                When provided, ``column`` and ``where`` are ignored.

        Returns:
            ColumnsOperation: The current instance with its ``_output``
            updated to represent the ``IN`` clause. This allows method chaining.

        Raises:
            Exception: If neither ``data_list`` nor a valid ``column``
                is provided.

        Example:
            Assuming ``users`` and ``admins`` tables::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', port=3306, username='root',
                            password='secret', db_name='my_app')
                users  = db.users
                admins = db.admins

                # Literal list mode (backward compatible)
                expr1 = users.name.In(['Alice', 'Bob'])
                # expr1._output[0] -> '(`users`.`name` IN (%s, %s))'
                # expr1._output[1] -> ['Alice', 'Bob']

                # Literal list mode (using keyword)
                expr2 = users.name.In(data_list=['Alice', 'Bob'])

                # Subquery mode with WHERE
                expr3 = users.name.In(
                    column=admins.username,
                    where=admins.active == True
                )
                # expr3._output[0] ->
                #   '(`users`.`name` IN (SELECT `admins`.`username` FROM `admins` WHERE (`admins`.`active` = %s)))'
                # expr3._output[1] -> [True]

                # Subquery mode without WHERE
                expr4 = users.name.In(column=admins.username)
                # expr4._output[0] ->
                #   '(`users`.`name` IN (SELECT `admins`.`username` FROM `admins`))'

                # Using the result in a query
                rows = users.get_row([users.name], where=expr1)
        """
        if isinstance(column, list):
            data_list, column = column, None #So user can simply In(['Alice', 'Bob']) with out passing arguments
        if not column and not data_list:
            raise Exception("In() requires either data_list or column")
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} IN ({", ".join(["%s" for _ in data_list])}))', self._output[1] + data_list) if data_list is not None else (f'({self._output[0]} IN (SELECT {column.name if isinstance(column, Column) else column._output[0]} FROM {(column.name if isinstance(column, Column) else column.col_obj.name).split(".")[0]}{f" WHERE {where._output[0]}" if isinstance(where, ColumnsOperation) else f" WHERE {where.name}" if isinstance(where, Column) else ""}))', self._output[1] + ([] if isinstance(column, Column) else column._output[1]) + (where._output[1] if isinstance(where, ColumnsOperation) else [])) if isinstance(column, (Column, ColumnsOperation)) else None
        
        return new_op

    def not_In(self, column: Column|ColumnsOperation = None, where: ColumnsOperation = None, data_list: list = None):
        """Build an SQL ``NOT IN`` clause for the current column expression.

        This method generates a condition that checks whether the column's value
        is **not** present in a given list of literals or in the result of a
        subquery. The behaviour mirrors :meth:`In` but with the negation operator
        ``NOT IN``.

        Two modes are supported:

        * **Literal list mode**: When ``data_list`` is provided, generates a
        parameterised ``NOT IN (%s, %s, ...)`` clause using the literal values.
        For backward compatibility, if a list of plain values is passed as the
        first positional argument (``column``), it is automatically treated as
        ``data_list``.
        * **Subquery mode**: When ``column`` is provided as a single
        :class:`Column` or :class:`ColumnsOperation`, builds a
        ``NOT IN (SELECT ...)`` subquery. The table name is extracted from the
        provided column object, and an optional ``where`` condition can be
        applied inside the subquery — handled identically to
        :meth:`Table.get_row`.

        The result is stored in the instance's ``_output`` attribute as a tuple
        ``(sql_string, parameters)``, and the instance is returned to allow
        chaining.

        Args:
            column: A single :class:`Column` or :class:`ColumnsOperation`
                to use in the ``SELECT`` clause of the subquery. The table name
                is determined from this object. Do not pass a list of columns;
                if you need multiple conditions, chain them using ``&`` or ``|``.
                If a list of literals is passed, it is treated as ``data_list``.
            where: An optional :class:`ColumnsOperation` (or :class:`Column`
                for boolean columns) representing the ``WHERE`` condition for
                the subquery. Defaults to ``None``.
            data_list: A list of literal values for a direct ``NOT IN`` clause.
                When provided, ``column`` and ``where`` are ignored.

        Returns:
            ColumnsOperation: The current instance with its ``_output``
            updated to represent the ``NOT IN`` clause. This allows method
            chaining.

        Raises:
            Exception: If neither ``data_list`` nor a valid ``column``
                is provided.

        Example:
            Assuming ``users`` and ``admins`` tables::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', port=3306, username='root',
                            password='secret', db_name='my_app')
                users  = db.users
                admins = db.admins

                # Literal list mode (backward compatible)
                expr1 = users.name.not_In(['Alice', 'Bob'])
                # expr1._output[0] -> '(`users`.`name` NOT IN (%s, %s))'
                # expr1._output[1] -> ['Alice', 'Bob']

                # Literal list mode (using keyword)
                expr2 = users.name.not_In(data_list=['Alice', 'Bob'])

                # Subquery mode with WHERE
                expr3 = users.name.not_In(
                    column=admins.username,
                    where=admins.active == True
                )
                # expr3._output[0] ->
                #   '(`users`.`name` NOT IN (SELECT `admins`.`username` FROM `admins` WHERE (`admins`.`active` = %s)))'

                # Subquery mode without WHERE
                expr4 = users.name.not_In(column=admins.username)
                # expr4._output[0] ->
                #   '(`users`.`name` NOT IN (SELECT `admins`.`username` FROM `admins`))'
        """
        if isinstance(column, list):
            data_list, column = column, None #So user can simply In(['Alice', 'Bob']) with out passing arguments
        if not column and not data_list:
            raise Exception("In() requires either data_list or column")
        new_op = ColumnsOperation(self.col_obj)
        new_op._output = (f'({self._output[0]} NOT IN ({", ".join(["%s" for _ in data_list])}))', self._output[1] + data_list) if data_list is not None else (f'({self._output[0]} NOT IN (SELECT {column.name if isinstance(column, Column) else column._output[0]} FROM {(column.name if isinstance(column, Column) else column.col_obj.name).split(".")[0]}{f" WHERE {where._output[0]}" if isinstance(where, ColumnsOperation) else f" WHERE {where.name}" if isinstance(where, Column) else ""}))', self._output[1] + ([] if isinstance(column, Column) else column._output[1]) + (where._output[1] if isinstance(where, ColumnsOperation) else [])) if isinstance(column, (Column, ColumnsOperation)) else None
        
        return new_op

    def If(self, condition):
        """
        Start a one-line conditional expression: ``then if cond else other``.

        Returns a :class:`_IfThenBuilder`. Chain ``.Else(value)`` to obtain
        a usable :class:`ColumnsOperation`. Any attempt to read ``_output``
        on the builder without chaining ``.Else()`` raises a clear
        :class:`RuntimeError`.

        The SQL is rendered with MySQL's ``IF(cond, then, else)`` function.

        Args:
            condition: A :class:`ColumnsOperation`, a :class:`Column`, or a
                raw Python value (``True``, ``1``, ``'x'``, ...). Raw values
                are wrapped in :class:`LiteralValue` automatically.

        Returns:
            _IfThenBuilder: Intermediate builder; call ``.Else(value)`` next.

        Example:
            >>> users.name.If(users.is_active == True).Else('inactive')
            # SQL: (IF((`users`.`is_active` = %s), `users`.`name`, %s))
        """
        return _IfThenBuilder(
            self,
            condition if isinstance(condition, (ColumnsOperation, Column)) else LiteralValue(condition)
        )


class LiteralValue(ColumnsOperation):
    """
    Wrap a raw Python value inside a ColumnsOperation-shaped object.

    Used when a plain literal must be the left-hand side of a conditional
    (``LiteralValue('n/a').If(cond).Else(col)``) or any other expression
    chain. Subclasses :class:`ColumnsOperation` but deliberately does
    **not** call ``super().__init__()`` — the parent constructor would
    reset ``_output`` to ``('', [])`` and erase the wrapped value.

    Attributes:
        _output (tuple[str, list]): Always ``('%s', [wrapped_value])``.
        col_obj (_NullCol): Stub in place of a real parent Column.
        current_datatype (type): ``type(wrapped_value)``.
    """

    def __init__(self, value):
        """
        Wrap a Python value in a ColumnsOperation-compatible shell.

        Args:
            value: Any value that MySQLdb can bind later.

        Example:
            >>> LiteralValue('Alice')._output
            ('%s', ['Alice'])
            >>> LiteralValue(42)._output
            ('%s', [42])
        """
        self._output          = ('%s', [value])
        self.col_obj          = _NullCol
        self.current_datatype = type(value)


class _IfThenBuilder:
    """
    Intermediate builder returned by :meth:`ColumnsOperation.If`.

    The ``_output`` attribute is a read-only property that always raises
    :class:`RuntimeError` — this is what guarantees that a forgotten
    ``.Else()`` is caught before any SQL reaches the database.

    SQL syntax note:
        MySQL/MariaDB provide the ``IF(cond, then, else)`` function with
        the same argument order as the ORM's ternary syntax, so no extra
        rewriting is needed.

    Attributes:
        _then (ColumnsOperation): The "then" branch expression.
        _cond (ColumnsOperation | Column): The condition.
    """

    def __init__(self, then_branch, condition):
        self._then = then_branch
        self._cond = condition

    @property
    def _output(self):
        """Always raise — forces the user to chain ``.Else()``."""
        raise RuntimeError(
            "You called `.If(condition)` but never chained `.Else(value)`. "
            "Complete the conditional, e.g. `col.If(cond).Else(other)`."
        )

    def Else(self, value):
        """
        Complete the conditional with the "else" branch.

        Produces ``(IF(cond, then, else))`` with all parameters concatenated
        in the order: cond → then → else.

        Args:
            value: A :class:`ColumnsOperation`, :class:`Column`, or a raw
                Python value.

        Returns:
            ColumnsOperation: The fully-formed expression.
        """
        new_op = ColumnsOperation(self._then.col_obj)
        new_op._output = (
            f'(IF({self._cond._output[0]}, {self._then._output[0]}, {value._output[0]}))',
            self._cond._output[1] + self._then._output[1] + value._output[1]
        ) if isinstance(value, ColumnsOperation) else (
            f'(IF({self._cond._output[0]}, {self._then._output[0]}, {value.name}))',
            self._cond._output[1] + self._then._output[1]
        ) if isinstance(value, Column) else (
            f'(IF({self._cond._output[0]}, {self._then._output[0]}, %s))',
            self._cond._output[1] + self._then._output[1] + [value]
        )
        new_op.current_datatype = None
        return new_op


class Column:
    """
    Represents a database column and provides an expressive interface for building SQL expressions.

    The :class:`Column` class is the fundamental building block for query construction
    and schema manipulation. Each instance corresponds to a specific column in a
    database table and is typically created automatically by the :class:`Table`
    class when a table is loaded.

    **Expression Building**

    Columns support Python operators and methods that generate SQL expressions.
    These expressions are encapsulated in :class:`ColumnsOperation` objects,
    which can be chained together to build complex queries. The generated SQL
    is context-aware, using appropriate operators based on the column's data type
    (e.g., ``||`` for string concatenation vs. ``+`` for numeric addition).

    Supported operations include:

    - Arithmetic: ``+``, ``-``, ``*``, ``/``, ``%``, ``**`` (POW)
    - Comparisons: ``==``, ``!=``, ``>``, ``<``, ``>=``, ``<=`` (and explicit methods like ``.eq()``)
    - String methods: ``.like()``, ``.startswith()``, ``.endswith()``, ``.contains()``,
      ``.upper()``, ``.lower()``, ``.strip()``, ``.lstrip()``, ``.rstrip()``,
      ``.replace()``, ``.add_end()``, ``.add_first()``, and slice indexing
    - Set membership: ``.In()``
    - Logical combinations via ``&`` (AND) and ``|`` (OR)

    **Schema Modification**

    The class also provides methods for altering the table schema, such as
    :meth:`rename` and :meth:`delete_column`. These operations are destructive
    and require explicit confirmation flags.

    **Attributes**

    The Column instance stores the fully qualified column name (with table name),
    the unqualified name (for use in queries), a reference to its parent :class:`Table`,
    and the Python data type inferred from the database.

    Attributes:
        name (str): The fully qualified column name in the format
            ``'`table_name`.`column_name`'``. This is used in SQL expressions
            when the table needs to be explicitly referenced (e.g., in JOINs).
        first_name (str): The column name wrapped in backticks, e.g., ``'`column_name`'``.
            This is used when the table context is clear.
        table_obj (Table): The parent :class:`Table` instance that owns this column.
        datatype (type): The Python type corresponding to the column's SQL data type
            (e.g., ``int``, ``str``, ``float``, ``bytes``).

    Example:
        Accessing columns from a table instance and building expressions::

            from ormophine.Mysql import Table, Driver

            # Assume `db` is a Driver instance connected to a database
            users = db.users  # Table instance

            # Refer to columns as attributes
            age_col = users.age
            name_col = users.name

            # Build a condition using operators
            condition = (age_col >= 18) & name_col.startswith('A')
            # condition is a ColumnsOperation: ((users.age >= %s) AND (users.name like %s || '%%'))

            # Use string methods
            full_name = users.first_name.add_end(' ').add_end(users.last_name)
            # full_name._output[0] -> '((users.first_name || %s) || users.last_name)'

            # Rename a column (destructive)
            users.age.rename(users.age, 'user_age')

        For more details on available methods, refer to the individual method
        documentation.
    """
    def __init__(self, table_obj: Table, column_name: str, datatype: type):
        """
        Initialize a new Column instance representing a database column.

        This constructor is typically called automatically by the :class:`Table`
        class when it loads table metadata. Users rarely instantiate this class
        directly; instead, they access columns as attributes of a :class:`Table`
        instance (e.g., ``users.id``, ``users.name``).

        The column's fully qualified name (including the table name) is stored in
        :attr:`name`, while the raw column name wrapped in backticks is stored in
        :attr:`first_name` for use in SQL queries. The associated table object
        and the Python datatype (int, str, float, bytes) are also recorded.

        Args:
            table_obj (Table): The :class:`Table` instance that this column
                belongs to.
            column_name (str): The name of the column in the database table.
            datatype (type): The Python type corresponding to the column's SQL
                data type (e.g., ``int`` for INTEGER, ``str`` for VARCHAR, etc.).

        Returns:
            None

        Example:
            Columns are typically accessed via a table object::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', username='user',
                            password='pass', db_name='mydb')
                users = db.users  # Table instance

                # Accessing a column attribute returns a Column instance
                id_column = users.id
                # id_column.name -> '`users`.`id`'
                # id_column.first_name -> '`id`'
                # id_column.datatype -> int
        """
        self.name= table_obj.name_+'.`'+column_name+'`'
        self.first_name= f'`{column_name}`'
        self.table_obj= table_obj
        self.datatype= datatype

    def __hash__(self):
        """
        Return the hash value of the column.

        This method computes a hash based on the fully qualified column name
        (``table.column``). It enables :class:`Column` instances to be used as
        keys in dictionaries and sets. The hash is derived from the ``name``
        attribute, which uniquely identifies the column within the database.

        Returns:
            int: The hash value of the column's fully qualified name.

        Example:
            Using a :class:`Column` object as a dictionary key::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a column 'id'
                col = users.id
                d = {col: 'primary key'}
                print(d[col])  # 'primary key'
        """
        return hash(self.name)

    def __add__(self, value):
        """
        Implement addition or string concatenation for a column.

        This method is called when a :class:`Column` instance is used on the
        left side of the ``+`` operator. It creates a new :class:`ColumnsOperation`
        instance, initializes its internal SQL fragment with the column's fully
        qualified name, and then delegates the actual addition logic to the
        operation's own ``__add__`` method. The operator used depends on the
        column's datatype:

        - For string columns (``str``), the SQL ``||`` operator is used for
        concatenation.
        - For numeric columns, the SQL ``+`` operator is used for arithmetic
        addition.

        Args:
            value (ColumnsOperation, Column, int, float, str): The right-hand side
                of the addition. The type determines how the SQL is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance that
            represents the combined expression, allowing further chaining.

        Example:
            Assuming a ``users`` table with string column ``first_name`` and
            numeric column ``age``::

                # String concatenation
                full_name = users.first_name + ' ' + users.last_name
                # Resulting SQL: (users.first_name || %s)

                # Numeric addition
                next_age = users.age + 1
                # Resulting SQL: (users.age + %s)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob + value

    def __radd__(self, value):
        """
        Implement right-side addition (`value + self`) for a Column.

        This method is called when a :class:`Column` instance appears on the right
        side of a ``+`` operator (e.g., ``'prefix' + users.name``). It creates a
        new :class:`ColumnsOperation` instance associated with this column,
        initializes its internal SQL fragment to the column's qualified name, and
        then delegates the addition to the operation's ``__add__`` (or ``__radd__``)
        method, which handles the actual SQL generation based on the left operand's
        type.

        The result is a :class:`ColumnsOperation` that can be used in queries,
        conditions, or further chained operations.

        Args:
            value (Any): The left operand of the addition. Can be a string,
                number, :class:`Column`, or :class:`ColumnsOperation`. The type
                determines how the SQL expression is constructed (string
                concatenation for strings, arithmetic addition for numbers,
                etc.).

        Returns:
            ColumnsOperation: A new operation representing the addition of
                ``value`` and this column. This operation can be used in SQL
                expressions or comparisons.

        Example:
            Using right addition to concatenate a prefix with a column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a 'username' column
                expr = 'User: ' + users.username
                # expr is a ColumnsOperation representing
                # (%s || users.username) with parameter 'User: '
                # This can be used in a SELECT or WHERE clause.

                # Using with a numeric column:
                expr = 10 + users.age
                # expr is a ColumnsOperation representing
                # (%s + users.age) with parameter 10
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value + temp_ob

    def __sub__(self, value):
        """
        Implement subtraction between a column and another value.

        This method is called when the ``-`` operator is used with a :class:`Column`
        instance on the left side. It creates a new :class:`ColumnsOperation`
        instance initialized with the column's name, then delegates the subtraction
        to that operation's ``__sub__`` method, which generates the appropriate SQL
        expression.

        The result is a :class:`ColumnsOperation` that represents the subtraction
        operation, allowing further chaining of operations.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The right-hand side
                of the subtraction. The type determines how the SQL is generated:

                - If a :class:`ColumnsOperation`, the SQL fragments are combined
                with a minus sign.
                - If a :class:`Column`, the column name is used directly.
                - If an ``int`` or ``float``, a placeholder ``%s`` is used and
                the value is added to the parameters list.
                - Any other type is treated as a literal value.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the subtraction expression. Its ``_output`` attribute contains the
            SQL fragment and parameter list.

        Example:
            Subtracting a constant from a column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a column `age`
                result = users.age - 5
                # result._output[0] -> '(users.age - %s)'
                # result._output[1] -> [5]

                # Subtracting another column
                result = users.current_age - users.birth_year
                # result._output[0] -> '(users.current_age - users.birth_year)'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob - value

    def __rsub__(self, value):
        """
        Implement right-side subtraction (`other - self`) for a column.

        This method is called when a :class:`Column` instance appears on the right
        side of the ``-`` operator (e.g., ``100 - users.age``). It creates a
        :class:`ColumnsOperation` instance from the column and then performs
        a subtraction operation where the left operand is ``value`` and the
        right operand is the column's expression.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The left operand
                of the subtraction. Its type determines how the SQL expression
                is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the subtraction operation between ``value`` and this column.

        Example:
            Using a column in a right-side subtraction::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = 100 - users.age
                # op is a ColumnsOperation representing (100 - users.age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value - temp_ob

    def __mul__(self, value):
        """
        Implement multiplication (`self * other`) for a column.

        This method is called when a :class:`Column` instance is multiplied by
        another value using the ``*`` operator. It creates a :class:`ColumnsOperation`
        instance from the column and then performs a multiplication operation
        where the column is the left operand and ``value`` is the right operand.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The right operand
                of the multiplication. Its type determines how the SQL expression
                is constructed. If it is a :class:`ColumnsOperation` or :class:`Column`,
                its SQL fragment is used directly; otherwise, a placeholder ``%s``
                is used and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the multiplication operation between this column and ``value``.

        Example:
            Using a column in a multiplication expression::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `salary` column
                op = users.salary * 1.1
                # op is a ColumnsOperation representing (users.salary * %s)
                # with parameter 1.1
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob * value

    def __rmul__(self, value):
        """
        Implement right-side multiplication (`other * self`) for a column.

        This method is called when a :class:`Column` instance appears on the right
        side of the ``*`` operator (e.g., ``100 * users.age``). It creates a
        :class:`ColumnsOperation` instance from the column and then performs a
        multiplication operation where the left operand is ``value`` and the
        right operand is the column's expression.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The left operand
                of the multiplication. Its type determines how the SQL expression
                is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the multiplication operation between ``value`` and this column.

        Example:
            Using a column in a right-side multiplication::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = 2 * users.age
                # op is a ColumnsOperation representing (2 * users.age)
        """ 
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value * temp_ob

    def __pow__(self, value):
        """
        Implement the exponentiation operator (`**`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``**`` operator (e.g., ``users.age ** 2``). It creates a
        :class:`ColumnsOperation` instance from the column and then invokes
        the corresponding ``__pow__`` method of that operation with the provided
        value. The resulting :class:`ColumnsOperation` object contains the SQL
        fragment using the ``POW()`` function and the associated parameters.

        The exponentiation is performed using the SQL ``POW(base, exponent)``
        function. The base is the column expression, and the exponent is the
        given value (which may be a constant, another column, or a complex
        expression).

        Args:
            value (ColumnsOperation, Column, int, float, Any): The exponent to
                raise the column to. Its type determines how the SQL expression
                is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the exponentiation operation between this column and the given value.

        Example:
            Using the exponentiation operator on a numeric column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = users.age ** 2
                # op is a ColumnsOperation representing POW(users.age, %s)
                # with parameter [2]

                # Using a column as exponent
                op = users.age ** users.experience
                # op represents POW(users.age, users.experience)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob ** value

    def __rpow__(self, value):
        """
        Implement right-side exponentiation (`value ** self`) for a column.

        This method is called when a :class:`Column` instance appears on the right
        side of the ``**`` operator (e.g., ``2 ** users.age``). It creates a
        :class:`ColumnsOperation` instance from the column and delegates the
        actual SQL generation to the operation's ``__rpow__`` method.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment using the ``POW()`` function and the parameter list,
        which can be used in queries.

        Args:
            value (Any): The left operand (the base) for the exponentiation.
                This can be a constant, another column, or a
                :class:`ColumnsOperation` expression.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the power operation between ``value`` and this column.

        Example:
            Using a column in a right-side exponentiation::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = 2 ** users.age
                # op is a ColumnsOperation representing POW(%s, users.age)
                # with parameter 2
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value ** temp_ob

    def __truediv__(self, value):
        """
        Implement division (`self / value`) for a column.

        This method is called when a :class:`Column` instance is divided by a value
        using the ``/`` operator. It creates a :class:`ColumnsOperation` instance
        from the column and then performs a division operation where the column's
        expression is the numerator and ``value`` is the denominator.

        The returned :class:`ColumnsOperation` object contains the generated SQL
        fragment (using the ``/`` operator) and the parameter list, which can be
        used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The right operand
                (denominator) of the division. Its type determines how the SQL
                expression is constructed:
                - If a :class:`ColumnsOperation`, its SQL fragment and parameters
                are merged.
                - If a :class:`Column`, its name is used directly.
                - Otherwise, a placeholder ``%s`` is used and the value is added
                to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the division operation between this column and ``value``.

        Example:
            Using a column in a division::

                from ormophine.Mysql import Table

                # Assume `products` is a Table instance with `price` and `quantity` columns
                # Calculate price per unit
                op = products.price / products.quantity
                # op is a ColumnsOperation representing (products.price / products.quantity)

                # Division by a literal
                op = products.total / 100
                # op._output[0] -> '(products.total / %s)'
                # op._output[1] -> [100]
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob / value

    def __rtruediv__(self, value):
        """
        Implement right-side true division (`other / self`) for a column.

        This method is called when a :class:`Column` instance appears on the right
        side of the ``/`` operator (e.g., ``100 / users.age``). It creates a
        :class:`ColumnsOperation` instance from the column and then performs
        a division operation where the left operand is ``value`` and the right
        operand is the column's expression.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The left operand
                of the division. Its type determines how the SQL expression
                is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the division operation between ``value`` and this column.

        Example:
            Using a column in a right-side division::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = 100 / users.age
                # op is a ColumnsOperation representing (100 / users.age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value / temp_ob

    def __mod__(self, value):
        """
        Implement the modulo operator (`%`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``%`` operator (e.g., ``users.age % 2``). It creates a
        :class:`ColumnsOperation` instance that represents the SQL modulo
        operation. The resulting expression can be used in queries, WHERE
        clauses, or as part of larger expressions.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The right-hand
                operand of the modulo operation. Its type determines how the
                SQL expression is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the modulo operation between this column and the provided value.

        Example:
            Using the modulo operator to filter even ages::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age % 2 == 0
                # condition is a ColumnsOperation representing (users.age % 2) = %s
                # with parameter 0

            Using modulo in a column expression::

                op = users.age % 10
                # op is a ColumnsOperation representing (users.age % 10)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob % value

    def __rmod__(self, value):
        """
        Implement right-side modulo (`other % self`) for a column.

        This method is called when a :class:`Column` instance appears on the right
        side of the ``%`` operator (e.g., ``5 % users.age``). It creates a
        :class:`ColumnsOperation` instance from the column and then performs
        a modulo operation where the left operand is ``value`` and the right
        operand is the column's expression.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, int, float, Any): The left operand
                of the modulo operation. Its type determines how the SQL expression
                is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the modulo operation between ``value`` and this column.

        Example:
            Using a column in a right-side modulo::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                op = 10 % users.age
                # op is a ColumnsOperation representing (10 % users.age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return value % temp_ob

    def eq(self, value):
        """
        Create an equality comparison expression for this column.

        This method generates a SQL equality condition where this column is compared
        to the provided value. It returns a :class:`ColumnsOperation` object that
        contains the SQL fragment and the associated parameters.

        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, its SQL fragment is used on
        the right side, and its parameters are merged.
        - If ``value`` is a :class:`Column`, its fully qualified name is used directly,
        and no additional parameters are added.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is used,
        and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                equality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NULL``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the equality condition ``self.name = value``.

        Example:
            Assuming a :class:`Table` instance ``users`` with a column ``id``::

                # Compare with a literal value
                condition = users.id.eq(42)
                # condition._output[0] -> '(users.id = %s)'
                # condition._output[1] -> [42]

                # Compare with another column
                condition = users.id.eq(orders.user_id)
                # condition._output[0] -> '(users.id = orders.user_id)'
                # condition._output[1] -> []

                # Compare with a ColumnsOperation (e.g., subquery or expression)
                subquery = users.id > 100
                condition = users.id.eq(subquery)
                # condition._output[0] -> '(users.id = (users.id > %s))'
                # condition._output[1] -> [100]
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} = {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} = {value.name})', []) if isinstance(value, Column) else (f'({self.name} IS NULL)', []) if value is None else (f'({self.name} = %s)', [value])
        return temp_ob

    def __eq__(self, value):
        """
        Implement equality comparison (`self == value`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``==`` operator. It creates a :class:`ColumnsOperation` instance that
        represents an equality condition between the column and the provided value.
        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, the condition is
        ``column == operation``, and its SQL fragment and parameters are used.
        - If ``value`` is a :class:`Column`, the condition is
        ``column == other_column``, and both column names are used directly.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and parameter list, which can be used in queries.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                equality comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NULL``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the equality condition between this column and ``value``.

        Example:
            Using equality comparison in a query::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                condition = users.name == 'Alice'
                # condition is a ColumnsOperation with SQL: '(users.name = %s)'
                # and parameters: ['Alice']

                # Comparing two columns
                condition = users.id == users.manager_id
                # SQL: '(users.id = users.manager_id)'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} = {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} = {value.name})', []) if isinstance(value, Column) else (f'({self.name} IS NULL)', []) if value is None else (f'({self.name} = %s)', [value])
        return temp_ob

    def ne(self, value):
        """
        Create a not-equal comparison expression.

        This method generates a SQL inequality operation between the column
        and the provided value. It returns a :class:`ColumnsOperation` instance
        that can be used in WHERE clauses or combined with other conditions.
        The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, the column is compared
        to the SQL expression of that operation, and parameters are merged.
        - If ``value`` is a :class:`Column`, the comparison uses the column
        name directly, with no additional parameters.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added as a parameter.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                not-equal comparison. Its type determines how the SQL expression
                and parameters are constructed or ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NOT NULL``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the inequality ``self.name != value``.

        Example:
            Comparing a column to a literal value::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age.ne(25)
                # condition._output[0] -> '(users.age != %s)'
                # condition._output[1] -> [25]

            Comparing two columns::

                condition = users.age.ne(users.max_age)
                # condition._output[0] -> '(users.age != users.max_age)'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} != {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} != {value.name})', []) if isinstance(value, Column) else (f'({self.name} IS NOT NULL)', []) if value is None else (f'({self.name} != %s)', [value])
        return temp_ob

    def __ne__(self, value):
        """
        Implement the not‑equal comparison operator (`!=`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``!=`` operator (e.g., ``users.age != 25``). It creates a
        :class:`ColumnsOperation` object that represents the SQL inequality
        condition. The right‑hand side can be a constant value, another column,
        or a more complex expression (e.g., a :class:`ColumnsOperation` instance).

        The returned :class:`ColumnsOperation` contains the generated SQL
        fragment (with placeholders for parameters) and a list of parameter
        values, which can be used in queries like :meth:`Table.get_row` or
        :meth:`Table.update`.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                inequality comparison. Its type determines how the SQL expression
                is constructed:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), a placeholder
                ``%s`` is used, and the value is added to the parameter list or 
                ``None``. When ``None`` is passed, the generated SQL
                becomes ``<expression> IS NOT NULL``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the inequality condition ``self != value``, ready for use in SQL
            queries.

        Example:
            Using the ``!=`` operator to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age != 30
                # condition is a ColumnsOperation representing (users.age != %s)
                # with parameter [30]

                # Comparing two columns
                condition = users.age != users.max_age
                # condition is a ColumnsOperation representing (users.age != users.max_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} != {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} != {value.name})', []) if isinstance(value, Column) else (f'({self.name} IS NOT NULL)', []) if value is None else (f'({self.name} != %s)', [value])
        return temp_ob

    def gt(self, value):
        """
        Create a greater-than comparison expression for the column.

        This method generates a SQL ``>`` (greater than) operation between the
        column and the provided value. It returns a :class:`ColumnsOperation`
        instance containing the SQL fragment and parameters, which can be used in
        queries (e.g., :meth:`Table.get_row`, :meth:`Table.update`).

        The right-hand side ``value`` can be:
        - A :class:`ColumnsOperation` (complex expression or subquery)
        - A :class:`Column` (another column, for column-to-column comparison)
        - A constant (int, float, str, etc.), which will be parameterized.

        Args:
            value (ColumnsOperation, Column, Any): The right-hand side of the
                greater-than comparison. Its type determines how the SQL
                expression is constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self > value``, ready for use in SQL queries.

        Example:
            Comparing a column to a constant::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age.gt(18)
                # condition._output[0] -> '(users.age > %s)'
                # condition._output[1] -> [18]

                # Comparing two columns
                condition = users.age.gt(users.min_age)
                # condition._output[0] -> '(users.age > users.min_age)'

                # Using with a ColumnsOperation
                avg_age = (users.age + users.age2) / 2
                condition = users.age.gt(avg_age)
                # condition._output[0] -> '(users.age > ((users.age + users.age2) / 2))'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} > {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} > {value.name})', []) if isinstance(value, Column) else (f'({self.name} > %s)', [value])
        return temp_ob

    def __gt__(self, value):
        """
        Implement the greater‑than comparison operator (`>`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``>`` operator (e.g., ``users.age > 25``). It creates a
        :class:`ColumnsOperation` object that represents the SQL greater‑than
        condition. The right‑hand side can be a constant value, another column,
        or a more complex expression (e.g., a :class:`ColumnsOperation` instance).

        The returned :class:`ColumnsOperation` contains the generated SQL
        fragment (with placeholders for parameters) and a list of parameter
        values, which can be used in queries like :meth:`Table.get_row` or
        :meth:`Table.update`.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                greater‑than comparison. Its type determines how the SQL expression
                is constructed:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), a placeholder
                ``%s`` is used, and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self > value``, ready for use in SQL queries.

        Example:
            Using the ``>`` operator to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age > 30
                # condition is a ColumnsOperation representing (users.age > %s)
                # with parameter [30]

                # Comparing two columns
                condition = users.age > users.min_age
                # condition is a ColumnsOperation representing (users.age > users.min_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} > {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} > {value.name})', []) if isinstance(value, Column) else (f'({self.name} > %s)', [value])
        return temp_ob

    def lt(self, value):
        """
        Create a less‑than comparison expression for this column.

        This method generates a SQL ``<`` (less‑than) condition between the column
        and the provided value. It creates a :class:`ColumnsOperation` object
        containing the SQL fragment and parameter list, which can be used in
        queries such as :meth:`Table.get_row` or :meth:`Table.update`.

        The right‑hand side can be a constant, another column, or a more complex
        expression. The behavior depends on the type of ``value``:

        - If ``value`` is a :class:`ColumnsOperation`, its SQL fragment is used
        directly, and its parameters are merged with the column's (none).
        - If ``value`` is a :class:`Column`, the column name is used directly,
        with no additional parameters.
        - For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                less‑than comparison. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self < value``, ready for use in SQL queries.

        Example:
            Filtering rows where age is less than 30::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age.lt(30)
                # condition represents: users.age < %s with parameter [30]

                # Comparing two columns
                condition = users.age.lt(users.max_age)
                # condition represents: users.age < users.max_age
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} < {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} < {value.name})', []) if isinstance(value, Column) else (f'({self.name} < %s)', [value])
        return temp_ob

    def __lt__(self, value):
        """
        Implement the less‑than comparison operator (`<`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``<`` operator (e.g., ``users.age < 25``). It creates a
        :class:`ColumnsOperation` object that represents the SQL less‑than
        condition. The right‑hand side can be a constant value, another column,
        or a more complex expression (e.g., a :class:`ColumnsOperation` instance).

        The returned :class:`ColumnsOperation` contains the generated SQL
        fragment (with placeholders for parameters) and a list of parameter
        values, which can be used in queries like :meth:`Table.get_row` or
        :meth:`Table.update`.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                less‑than comparison. Its type determines how the SQL expression
                is constructed:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), a placeholder
                ``%s`` is used, and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the less‑than condition ``self < value``, ready for use in SQL queries.

        Example:
            Using the ``<`` operator to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age < 30
                # condition is a ColumnsOperation representing (users.age < %s)
                # with parameter [30]

                # Comparing two columns
                condition = users.age < users.max_age
                # condition is a ColumnsOperation representing (users.age < users.max_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} < {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} < {value.name})', []) if isinstance(value, Column) else (f'({self.name} < %s)', [value])
        return temp_ob

    def ge(self, value):
        """
        Create a greater‑than‑or‑equal comparison expression.

        This method generates a SQL ``>=`` operation between the column and the
        provided value. It creates a new :class:`ColumnsOperation` instance
        containing the SQL fragment and parameter list. The behavior depends on
        the type of ``value``:

        * If ``value`` is a :class:`ColumnsOperation`, its SQL fragment and
        parameters are used directly.
        * If ``value`` is a :class:`Column`, the column name is used directly,
        and no additional parameters are added.
        * For any other type (e.g., int, float, str), a placeholder ``%s`` is
        used, and the value is added to the parameter list.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                comparison. Its type determines how the SQL expression and
                parameters are constructed.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self >= value``, ready for use in queries.

        Example:
            Using the ``ge`` method to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age.ge(18)
                # condition is a ColumnsOperation representing (users.age >= %s)
                # with parameter [18]

                # Comparing two columns
                condition = users.age.ge(users.min_age)
                # condition is a ColumnsOperation representing (users.age >= users.min_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} >= {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} >= {value.name})', []) if isinstance(value, Column) else (f'({self.name} >= %s)', [value])
        return temp_ob

    def __ge__(self, value):
        """
        Implement the greater-than-or-equal comparison operator (`>=`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``>=`` operator (e.g., ``users.age >= 18``). It creates a
        :class:`ColumnsOperation` object that represents the SQL condition
        ``self >= value``. The right‑hand side can be a constant value,
        another column, or a more complex expression (a :class:`ColumnsOperation`
        instance). The returned object contains the generated SQL fragment and
        the associated parameter list, suitable for use in queries like
        :meth:`Table.get_row` or :meth:`Table.update`.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                comparison. Its type determines how the SQL expression is built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), a placeholder
                ``%s`` is used, and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self >= value``, ready for use in SQL queries.

        Example:
            Using the ``>=`` operator to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age >= 18
                # condition is a ColumnsOperation representing (users.age >= %s)
                # with parameter [18]

                # Comparing two columns
                condition = users.age >= users.min_age
                # condition is a ColumnsOperation representing (users.age >= users.min_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} >= {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} >= {value.name})', []) if isinstance(value, Column) else (f'({self.name} >= %s)', [value])
        return temp_ob

    def le(self, value):
        """
        Create a less-than-or-equal comparison expression for the column.

        This method generates a SQL condition representing ``self <= value``.
        It creates a :class:`ColumnsOperation` object that contains the SQL
        fragment (with placeholders as needed) and the associated parameter list.
        The right‑hand side can be a constant, another column, or a complex
        expression (a :class:`ColumnsOperation` instance).

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                comparison. The behavior depends on the type:
                - :class:`ColumnsOperation`: its SQL fragment is used directly,
                and its parameters are merged with the current ones.
                - :class:`Column`: the column name is used directly; no additional
                parameters.
                - Other (e.g., int, float, str): a placeholder ``%s`` is inserted,
                and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self <= value``, ready for use in SQL queries.

        Example:
            Filtering rows where age is at most 30::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age.le(30)
                # condition._output[0] -> '(users.age <= %s)'
                # condition._output[1] -> [30]

                # Comparing two columns
                condition = users.age.le(users.max_age)
                # condition._output[0] -> '(users.age <= users.max_age)'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} <= {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} <= {value.name})', []) if isinstance(value, Column) else (f'({self.name} <= %s)', [value])
        return temp_ob

    def __le__(self, value):
        """
        Implement the less-than-or-equal comparison operator (`<=`) for a column.

        This method is called when a :class:`Column` instance is used with the
        ``<=`` operator (e.g., ``users.age <= 30``). It creates a
        :class:`ColumnsOperation` object that represents the SQL condition
        ``self <= value``. The right‑hand side can be a constant value,
        another column, or a more complex expression (a :class:`ColumnsOperation`
        instance). The returned object contains the generated SQL fragment and
        the associated parameter list, suitable for use in queries like
        :meth:`Table.get_row` or :meth:`Table.update`.

        Args:
            value (ColumnsOperation, Column, Any): The right‑hand side of the
                comparison. Its type determines how the SQL expression is built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged (though in this case,
                the left side is the column and the right side is the operation).
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), a placeholder
                ``%s`` is used, and the value is added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self <= value``, ready for use in SQL queries.

        Example:
            Using the ``<=`` operator to filter rows::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `age` column
                condition = users.age <= 30
                # condition is a ColumnsOperation representing (users.age <= %s)
                # with parameter [30]

                # Comparing two columns
                condition = users.age <= users.max_age
                # condition is a ColumnsOperation representing (users.age <= users.max_age)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} <= {value._output[0]})', value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} <= {value.name})', []) if isinstance(value, Column) else (f'({self.name} <= %s)', [value])
        return temp_ob

    def __getitem__(self, key: slice):
        """
        Just like python string slicing , implement Python's slice syntax (`column[start:stop]`) to generate SQL substring expressions.

        This method enables string column slicing using Python's bracket notation, converting
        the slice indices into a SQL ``SUBSTRING()`` function call. The behavior mimics Python
        string slicing with support for positive, negative, and omitted indices, but is
        translated to SQL's 1‑based indexing and parameterised placeholders.

        The returned :class:`ColumnsOperation` object contains the generated SQL fragment
        and the associated parameter list, ready for use in queries.

        The mapping from Python slice semantics to SQL is as follows:

        - `col[:]` → ``SUBSTRING(col, 1, LENGTH(col) + 1)`` (returns the whole string,
        though the +1 is a quirk of the implementation)
        - `col[:stop]` with `stop >= 0` → ``SUBSTRING(col, 1, stop)``
        - `col[:stop]` with `stop < 0` → ``SUBSTRING(col, 1, LENGTH(col) - abs(stop))``
        - `col[start:]` with `start >= 0` → ``SUBSTRING(col, start + 1, LENGTH(col))``
        - `col[start:]` with `start < 0` → ``SUBSTRING(col, LENGTH(col) - abs(start) - 1, LENGTH(col))``
        - `col[start:stop]` with `start >= 0, stop > 0` → ``SUBSTRING(col, start + 1, stop - start)``
        - `col[start:stop]` with `start >= 0, stop < 0` → ``SUBSTRING(col, start + 1, LENGTH(col) - abs(stop - start))``
        - Other combinations follow similar logic with adjustments for 1‑based indexing and length computations.

        Args:
            key (slice): A Python slice object with optional ``start``, ``stop``, and
                ``step`` attributes. The ``step`` is ignored (not supported in SQL
                SUBSTRING). The indices are interpreted as character positions.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the substring expression, with its ``_output`` attribute set to
            ``(sql_fragment, parameters)``.

        Raises:
            None: This method does not raise exceptions directly, though invalid
            slice types (non‑slice) would result in an error at runtime.

        Example:
            Slicing a string column to get the first 3 characters::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                op = users.name[:3]
                # op._output[0] -> '(SUBSTRING(users.name , 1 , %s))'
                # op._output[1] -> [3]

                # Slicing from the 2nd character to the 5th
                op = users.name[1:5]
                # op._output[0] -> '(SUBSTRING(users.name , %s , %s))'
                # op._output[1] -> [2, 4]  # start=2 (1-based), length=4

                # Negative stop: exclude last 2 characters
                op = users.name[:-2]
                # op._output[0] -> '(SUBSTRING(users.name , 1 , LENGTH(users.name) - %s))'
                # op._output[1] -> [2]  # abs(stop)
        """
        temp_ob = ColumnsOperation(self)
        if key.start == None and key.stop ==  None:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , 1 , LENGTH({temp_ob.col_obj.name}) + 1))', [])   #
        elif key.start == None and key.stop < 0:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , 1 , LENGTH({temp_ob.col_obj.name}) - %s))', [abs(key.stop)])  #
        elif key.start == None and key.stop >= 0:
             temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , 1 , %s))', [key.stop])  #  
        elif key.start >= 0 and key.stop ==  None:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , %s , LENGTH({temp_ob.col_obj.name})))', [key.start + 1])  #   
        elif key.start < 0 and key.stop == None:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , LENGTH({temp_ob.col_obj.name}) - %s , LENGTH({temp_ob.col_obj.name})))', [abs(key.start) - 1])  #
        elif key.start >= 0 and key.stop < 0:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , %s , LENGTH({temp_ob.col_obj.name}) - %s))', [key.start + 1, abs(key.stop - key.start)])  #  
        elif key.start >= 0 and key.stop > 0:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , %s , %s))', [key.start + 1, key.stop - key.start])  #
        elif key.start < 0 and key.stop < 0:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , LENGTH({temp_ob.col_obj.name}) - %s , %s))', [abs(key.start) - 1, key.stop - key.start])  #
        elif key.start < 0 and key.stop > 0:
            temp_ob._output = (f'(SUBSTRING({temp_ob.col_obj.name} , LENGTH({temp_ob.col_obj.name}) - %s ,  %s - (LENGTH({temp_ob.col_obj.name}) - %s)))', [abs(key.start) - 1, key.stop, abs(key.start)])
        return temp_ob

    def strip(self, chars: str = ' '):
        """
        Just like python strip(), generate a SQL expression that trims both leading and trailing characters from a column.

        This method applies the SQL ``TRIM(BOTH ... FROM ...)`` function to the
        column, removing all leading and trailing occurrences of the specified
        characters. The result is a :class:`ColumnsOperation` object that can be
        used in queries, updates, or further chained operations.

        Args:
            chars (str, optional): A string of characters to remove from both
                ends of the column value. Defaults to a single space ``' '``.
                If multiple characters are provided, each is treated as a separate
                character to be stripped.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance whose SQL
            fragment represents the trimmed column, and whose parameter list is
            empty (since the character set is embedded in the SQL). This object
            can be used directly in queries or further manipulated.

        Example:
            Removing leading/trailing spaces from a ``username`` column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `username` column
                trimmed = users.username.strip()
                # trimmed._output[0] -> "(TRIM(BOTH ' ' FROM users.username))"
                # trimmed._output[1] -> []

                # Using with a condition
                rows = users.get_row(
                    which_columns=[users.id, users.username],
                    where=users.username.strip() == 'admin'
                )
                # This generates SQL: ... WHERE ((TRIM(BOTH ' ' FROM users.username)) = %s)

                # Strip other characters, e.g., underscores
                trimmed_underscore = users.username.strip('_')
                # trimmed_underscore._output[0] -> "(TRIM(BOTH '_' FROM users.username))"
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"(TRIM(BOTH '{chars}' FROM {temp_ob._output[0]}))", temp_ob._output[1]) if temp_ob._output[0] else (f"(TRIM(BOTH '{chars}' FROM {temp_ob.col_obj.name}))", [])
        return temp_ob

    def lstrip(self, chars: str = ' '):
        """
        Just like python lstrip(), generate a SQL expression that trims leading characters from a column value.

        This method applies the SQL ``TRIM(LEADING ... FROM ...)`` function to the
        column, removing all leading occurrences of the specified characters from
        the column's value. The result is a :class:`ColumnsOperation` object that
        can be used in queries, updates, or combined with other operations.

        The method creates a new :class:`ColumnsOperation` instance wrapping the
        column, then updates its internal ``_output`` with the SQL fragment and
        parameter list. The operation is chainable with other :class:`ColumnsOperation`
        methods.

        Args:
            chars (str, optional): A string of characters to remove from the
                leading end of the column value. If multiple characters are
                provided, each is treated as a separate character to be stripped.
                Defaults to a single space ``' '``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the left‑trim operation on the column. The operation's SQL fragment
            will be something like ``TRIM(LEADING ' ' FROM column_name)``, and
            its parameter list will be empty (since the character set is inlined
            in the SQL).

        Example:
            Using ``lstrip()`` to remove leading spaces from a name column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                op = users.name.lstrip()
                # op._output[0] -> "(TRIM(LEADING ' ' FROM users.name))"
                # op._output[1] -> []

                # Remove leading '@' characters
                op = users.username.lstrip('@')
                # op._output[0] -> "(TRIM(LEADING '@' FROM users.username))"

                # Combine with other operations
                op = users.name.upper().lstrip()
                # op._output[0] -> "(TRIM(LEADING ' ' FROM UPPER(users.name)))"
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"(TRIM(LEADING '{chars}' FROM {temp_ob._output[0]}))", temp_ob._output[1]) if temp_ob._output[0] else (f"(TRIM(LEADING '{chars}' FROM {temp_ob.col_obj.name}))", [])
        return temp_ob

    def rstrip(self, chars: str = ' '):
        """
        Just like python rstrip(), generate a SQL expression that trims trailing characters from a string column.

        This method applies the SQL ``TRIM(TRAILING ... FROM ...)`` function to
        the column, removing all trailing occurrences of the specified characters.
        The method returns a new :class:`ColumnsOperation` object that contains
        the generated SQL fragment and the associated parameter list.

        Args:
            chars (str, optional): A string of characters to remove from the
                trailing end of the column value. Defaults to a single space ``' '``.
                If multiple characters are provided, each is treated as a separate
                character to be stripped.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the right‑trim operation, ready to be used in queries or chained
            with other operations.

        Example:
            Using ``rstrip`` to remove trailing spaces from a column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                op = users.name.rstrip()
                # op._output[0] -> "(TRIM(TRAILING ' ' FROM users.name))"

                # Removing trailing '@' characters
                op = users.username.rstrip('@')
                # op._output[0] -> "(TRIM(TRAILING '@' FROM users.username))"
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"(TRIM(TRAILING '{chars}' FROM {temp_ob._output[0]}))", temp_ob._output[1]) if temp_ob._output[0] else (f"(TRIM(TRAILING '{chars}' FROM {temp_ob.col_obj.name}))", [])
        return temp_ob

    def add_end(self, content):
        """
        Generate a SQL expression that concatenates content to the end of this column.

        This method creates a :class:`ColumnsOperation` that represents the SQL
        string concatenation operation ``column || content``. It is intended for
        string columns and requires the MySQL ``PIPES_AS_CONCAT`` SQL mode to be
        enabled (the driver sets this automatically). The behavior depends on the
        type of ``content``:

        - If ``content`` is a :class:`ColumnsOperation`, its SQL fragment is used
        and its parameters are merged.
        - If ``content`` is a :class:`Column`, the column name is used directly,
        with no additional parameters.
        - For any other type (e.g., str, int), a placeholder ``%s`` is used, and
        the value is added to the parameter list.

        Args:
            content (ColumnsOperation, Column, Any): The content to append to the
                column. Its type determines how the SQL expression is built.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the concatenation expression, ready for use in queries.

        Example:
            Concatenating a space and a last name to a first name column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with `first_name` and `last_name` columns
                full_name = users.first_name.add_end(' ').add_end(users.last_name)
                # full_name._output[0] -> '((users.first_name || %s) || users.last_name)'
                # full_name._output[1] -> [' ']
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({self.name} || {content._output[0]})', content._output[1]) if isinstance(content, ColumnsOperation) else (f'({self.name} || {content.name})', []) if isinstance(content, Column) else (f'({self.name} || %s)', [content])
        return temp_ob

    def add_first(self, content):
        """
        Generate a SQL expression that prepends content to the column's value.

        This method creates a string concatenation operation using the SQL ``||``
        operator, placing the provided ``content`` before the current column's
        value. The returned :class:`ColumnsOperation` object contains the generated
        SQL fragment and the associated parameter list, suitable for use in queries.

        The behavior depends on the type of ``content``:

        * If ``content`` is a :class:`ColumnsOperation`, its SQL fragment is used
        as the left operand, and its parameters are included.
        * If ``content`` is a :class:`Column`, its name is used directly, and no
        additional parameters are added.
        * For any other type (e.g., str, int), a placeholder ``%s`` is used, and
        the value is added to the parameter list.

        This method is intended for string columns. The SQL mode must have
        ``PIPES_AS_CONCAT`` enabled for ``||`` to perform concatenation.

        Args:
            content (ColumnsOperation, Column, Any): The content to prepend to
                the column's value. Its type determines how the SQL expression
                and parameters are constructed.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the concatenation operation, ready to be used in queries or chained
            with other operations.

        Example:
            Prepending a prefix to a column value::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                op = users.name.add_first('Mr. ')
                # op._output[0] -> '(%s || users.name)'
                # op._output[1] -> ['Mr. ']

                # Prepending another column
                op = users.first_name.add_first(users.title)
                # op._output[0] -> '(users.title || users.first_name)'
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'({content._output[0]} || {self.name})', content._output[1]) if isinstance(content, ColumnsOperation) else (f'({content.name} || {self.name})', []) if isinstance(content, Column) else (f'(%s || {self.name})', [content])
        return temp_ob
    
    def lower(self):
        """
        Just like python lower(), generate a SQL expression that converts the column value to lowercase.

        This method applies the SQL ``LOWER()`` function to the column, transforming
        all characters in the string value to lowercase. The method returns a new
        :class:`ColumnsOperation` object that contains the generated SQL fragment
        and the associated parameter list (which is empty for this operation).

        The returned object can be used in queries, conditions, or chained with
        other operations.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the lowercase operation, ready to be used in SQL queries.

        Example:
            Using ``lower()`` to perform a case‑insensitive comparison::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                condition = users.name.lower() == 'alice'
                # condition._output[0] -> '(LOWER(users.name) = %s)'
                # condition._output[1] -> ['alice']

                # Using in a query
                rows = users.get_row(
                    which_columns=[users.id, users.name],
                    where=condition
                )
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'(LOWER({temp_ob._output[0]}))', temp_ob._output[1]) if temp_ob._output[0] else (f'(LOWER({temp_ob.col_obj.name}))', [])
        return temp_ob

    def upper(self):
        """
        Just like python upper() , generate a SQL expression that converts the column value to uppercase.

        This method applies the SQL ``UPPER()`` function to the column,
        transforming all characters to their uppercase equivalent. It is
        typically used for case‑insensitive comparisons or formatting.

        The method returns a new :class:`ColumnsOperation` object that
        contains the generated SQL fragment and the associated parameter list.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the uppercase transformation, ready to be used in queries or
            chained with other operations.

        Example:
            Using ``upper`` to convert a column to uppercase in a query::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                op = users.name.upper()
                # op._output[0] -> '(UPPER(users.name))'

                # Using in a WHERE clause
                condition = users.name.upper() == 'ALICE'
                # condition._output[0] -> '((UPPER(users.name)) = %s)'
                # condition._output[1] -> ['ALICE']
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'(UPPER({temp_ob._output[0]}))', temp_ob._output[1]) if temp_ob._output[0] else (f'(UPPER({temp_ob.col_obj.name}))', [])
        return temp_ob

    def replace(self, old, new):
        """
        Just like python replace(), generate a SQL REPLACE operation on the column.

        This method creates a :class:`ColumnsOperation` that represents the SQL
        ``REPLACE()`` function, which replaces all occurrences of a substring
        within the column's value with a new substring. The operation is applied
        to the column directly, and the returned object can be used in queries
        or chained with other operations.

        Args:
            old (str): The substring to be replaced.
            new (str): The substring to replace with.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the ``REPLACE()`` function call, e.g.,
            ``REPLACE(column_name, %s, %s)``, with the appropriate parameters.

        Example:
            Replacing all occurrences of 'old' with 'new' in a column::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `bio` column
                op = users.bio.replace('old_text', 'new_text')
                # op._output[0] -> '(REPLACE(users.bio, %s, %s))'
                # op._output[1] -> ['old_text', 'new_text']
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f'(REPLACE({temp_ob._output[0]} , %s , %s))', temp_ob._output[1] + [old, new]) if temp_ob._output[0] else (f'(REPLACE({temp_ob.col_obj.name} , %s , %s))', [old, new])
        return temp_ob

    def like(self, value):
        """
        Generate a SQL ``LIKE`` condition for pattern matching on a string column.

        This method creates a :class:`ColumnsOperation` object that represents a
        SQL ``LIKE`` expression, allowing pattern-based filtering on string columns.
        The right‑hand side can be a constant string pattern, another column, or a
        more complex expression (a :class:`ColumnsOperation` instance). The method
        automatically converts the provided value to a string for safe parameter
        substitution.

        Args:
            value (ColumnsOperation, Column, Any): The pattern to match against
                the column. Its type determines how the SQL expression is built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), the value is
                converted to a string and a placeholder ``%s`` is used, with
                the value added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the ``LIKE`` condition ``self LIKE value``, ready for use in SQL
            queries.

        Example:
            Using the ``like`` method to perform pattern matching::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                # Find users whose name starts with 'A'
                condition = users.name.like('A%')
                # condition is a ColumnsOperation representing (users.name like %s)
                # with parameter ['A%']

                # Using another column as the pattern
                condition = users.name.like(users.pattern_column)
                # condition is a ColumnsOperation representing (users.name like users.pattern_column)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"({self.name} like {value._output[0]})", (temp_ob._output[1] + value._output[1]) if temp_ob._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f'({self.name} like {value.name})', temp_ob._output[1] if temp_ob._output[0] else []) if isinstance(value , Column) else (f'({self.name} like %s)', (temp_ob._output[1] + [f'{value}']) if temp_ob._output[0] else [f'{value}'])
        return temp_ob

    def startswith(self, value):
        """
        Just like python startswith(), generate a SQL ``LIKE`` condition that checks if the column starts with a given prefix.

        This method creates a :class:`ColumnsOperation` object that represents a SQL
        expression of the form ``column LIKE 'prefix%'``, which matches rows where
        the column value begins with the specified prefix. The prefix can be a
        constant string, another column, or a complex expression.

        The method automatically constructs the pattern by appending the SQL
        wildcard ``%`` to the provided prefix, ensuring that the condition
        matches any value that starts with the given string.

        Args:
            value (ColumnsOperation, Column, Any): The prefix to match at the start
                of the column's value. Its type determines how the SQL expression
                is built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., str, int), the value is converted to
                a string and a placeholder ``%s`` is used, with the value added
                to the parameter list. The pattern becomes ``%s || '%%'``.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the ``LIKE`` condition, ready to be used in queries or chained with
            other operations.

        Example:
            Using ``startswith`` to find users whose names begin with 'A'::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                condition = users.name.startswith('A')
                # condition._output[0] -> '(users.name like %s || '%%')'
                # condition._output[1] -> ['A']

                # Using another column as the prefix
                condition = users.name.startswith(users.prefix_column)
                # condition._output[0] -> '(users.name like users.prefix_column || '%%'')
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"({self.name} like {value._output[0]} || '%%')", (temp_ob._output[1] + value._output[1]) if temp_ob._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f"({self.name} like {value.name} || '%%')", temp_ob._output[1] if temp_ob._output[0] else []) if isinstance(value , Column) else (f"({self.name} like %s || '%%')", (temp_ob._output[1] + [f'{value}']) if temp_ob._output[0] else [f'{value}'])
        return temp_ob

    def endswith(self, value):
        """
        Just like python endswith(), generate a SQL condition that checks if a string column ends with a given suffix.

        This method creates a :class:`ColumnsOperation` object that represents a
        SQL ``LIKE`` expression with the pattern ``'%%' || value``, which matches
        strings that end with the specified suffix. The right‑hand side can be a
        constant string, another column, or a more complex expression (a
        :class:`ColumnsOperation` instance). The method automatically converts
        the provided value to a string for safe parameter substitution.

        Args:
            value (ColumnsOperation, Column, Any): The suffix to check against
                the column. Its type determines how the SQL expression is built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), the value is
                converted to a string and a placeholder ``%s`` is used, with
                the value added to the parameter list.

        Returns:
            ColumnsOperation: A new :class:`ColumnsOperation` instance representing
            the condition ``self LIKE '%%' || value``, ready for use in SQL queries.

        Example:
            Using ``endswith`` to filter rows whose column ends with a pattern::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `email` column
                condition = users.email.endswith('@example.com')
                # condition._output[0] -> "users.email like '%%' || %s"
                # condition._output[1] -> ['@example.com']

                # Using another column as the suffix
                condition = users.email.endswith(users.domain)
                # condition._output[0] -> "users.email like '%%' || users.domain"
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"({self.name} like '%%' || {value._output[0]})", (temp_ob._output[1] + value._output[1]) if temp_ob._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f"({self.name} like '%%' || {value.name})", temp_ob._output[1] if temp_ob._output[0] else []) if isinstance(value , Column) else (f"({self.name} like '%%' || %s)", (temp_ob._output[1] + [f'{value}']) if temp_ob._output[0] else [f'{value}'])
        return temp_ob

    def contains(self, value):
        """
        Generate a SQL condition that checks if a string column contains a substring.

        This method creates a :class:`ColumnsOperation` object representing a
        SQL ``LIKE`` condition with wildcards on both sides of the pattern:
        ``column LIKE '%value%'``. This checks whether the column's value contains
        the specified substring anywhere within it. The right‑hand side can be a
        constant string, another column, or a more complex expression.

        The method automatically handles the wildcard concatenation in the SQL
        expression, so you do not need to add ``%`` characters manually.

        Args:
            value (ColumnsOperation, Column, Any): The substring to search for
                within the column. Its type determines how the SQL expression is
                built:
                - If it is a :class:`ColumnsOperation`, its SQL fragment is used
                directly, and its parameters are merged.
                - If it is a :class:`Column`, the column name is used directly,
                with no additional parameters.
                - For any other type (e.g., int, float, str), the value is
                converted to a string and a placeholder ``%s`` is used, with
                the value added to the parameter list.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the ``LIKE`` condition with wildcards on both sides, ready for use
            in SQL queries.

        Example:
            Using ``contains`` to find rows where a column contains a substring::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with a `name` column
                # Find users whose name contains 'smith'
                condition = users.name.contains('smith')
                # condition._output[0] -> "users.name like '%%' || %s || '%%'"
                # condition._output[1] -> ['smith']

                # Using another column as the pattern
                condition = users.name.contains(users.search_term)
                # condition._output[0] -> "users.name like '%%' || users.search_term || '%%'"
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"({self.name} like '%%' || {value._output[0]} || '%%')", (temp_ob._output[1] + value._output[1]) if temp_ob._output[0] else value._output[1]) if isinstance(value, ColumnsOperation) else (f"({self.name} like '%%' || {value.name} || '%%')", temp_ob._output[1] if temp_ob._output[0] else []) if isinstance(value , Column) else (f"({self.name} like '%%' || %s || '%%')", (temp_ob._output[1] + [f'{value}']) if temp_ob._output[0] else [f'{value}'])
        return temp_ob

    def rename(self, column: 'Column', new_name: str) -> None:
        """
        Rename an existing column in the table.

        This method renames a column by executing an ``ALTER TABLE ... CHANGE COLUMN``
        statement. It first retrieves the full data type definition of the column
        (including any attributes like ``NOT NULL``, ``DEFAULT``, etc.) from the
        table's schema using :meth:`~Table.get_table_info`. Then it constructs and
        executes the SQL command to rename the column to the new name while preserving
        its data type and all constraints.

        After the database schema is updated, the method also updates the table
        object's dynamic attributes: it deletes the attribute corresponding to the
        old column name and creates a new attribute with the new name, maintaining
        a consistent Python representation of the table structure.

        Args:
            column (Column): The :class:`Column` object representing the column
                to be renamed. This object must belong to the current table.
            new_name (str): The new name for the column. Must be a valid SQL
                identifier (e.g., no spaces or special characters, unless properly
                quoted).

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., the column does
                not exist, the new name conflicts with an existing column,
                insufficient privileges, or a database error). The original error
                and the executed query are included in the exception message.

        Example:
            Renaming a column from ``'old_name'`` to ``'new_name'``::

                # Assume `users` is a Table instance
                old_col = users.old_name
                users.rename(old_col, 'new_name')
                # Now the column is accessible as users.new_name

            Note that after renaming, the old attribute is removed::

                # This would raise AttributeError
                users.old_name
        """
        for col_info in self.table_obj.get_table_info():
            if col_info['name'] == column.first_name[1:-1]:
                full_type = col_info['full_type']  
                break
        query = f'ALTER TABLE {self.table_obj.name_} CHANGE COLUMN {column.first_name} `{new_name}` {full_type};'
        self.table_obj._exc(query)
        self.table_obj.__delattr__(column.first_name[1:-1])
        self.table_obj.__setattr__(new_name, Column(self.table_obj, new_name, column.datatype))

    def delete_column(self, are_you_sure: bool, are_you_really_sure: bool, for_sure: bool) -> None:
        """
        Permanently delete this column from the table.

        This method executes an ``ALTER TABLE ... DROP COLUMN`` statement to remove
        the column from the database. To prevent accidental deletion, three explicit
        confirmation flags are required. All three must be ``True`` for the operation
        to proceed. After successful deletion, the column attribute is also removed
        from the parent :class:`Table` object.

        Args:
            are_you_sure (bool): First-level confirmation flag.
            are_you_really_sure (bool): Second-level confirmation flag.
            for_sure (bool): Final confirmation flag.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., insufficient
                privileges or the column does not exist). The original error and
                query are included in the exception message.

        Example:
            Assuming a :class:`Column` instance named ``users.age`` attached to a
            :class:`Table` instance ``users``::

                # Danger: this deletes the 'age' column
                users.age.delete_column(True, True, True)

            If any flag is ``False``, nothing happens::

                users.age.delete_column(True, True, False)  # No effect
        """
        if are_you_sure and are_you_really_sure and for_sure:
            query = f'ALTER TABLE {self.table_obj.name_} DROP COLUMN {self.first_name};'
            self.table_obj._exc(query)
            self.table_obj.__delattr__(self.first_name[1:-1])

    def In(self, column: 'Ormophine.Mysql.Column|Ormophine.Mysql.ColumnsOperation' = None, where: 'Ormophine.Mysql.ColumnsOperation' = None, data_list: list = None):
        """Build an SQL ``IN`` clause for the current column.

        This method serves as an entry point for the :class:`ColumnsOperation`
        ``In`` method. It initialises a :class:`ColumnsOperation` with the
        current column's name and delegates the execution to it, enabling
        seamless method chaining.

        Supports two modes:
        * Passing a list of literal values to ``data_list`` (or as the first
        positional argument for backward compatibility).
        * Passing a single :class:`Column`/:class:`ColumnsOperation` to
        ``column`` to build a ``SELECT`` subquery, with an optional
        ``where`` condition.

        Args:
            column: A single :class:`Column` or :class:`Ormophine.Mysql.ColumnsOperation`
                to use in the ``SELECT`` clause of the subquery. If a list of
                literals is passed, it is treated as ``data_list``.
            where: An optional :class:`Ormophine.Mysql.ColumnsOperation` (or
                :class:`Column`) representing the ``WHERE`` condition for the
                subquery.
            data_list: A list of literal values for a direct ``IN`` clause.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the ``IN`` clause, allowing further chaining.

        Raises:
            Exception: If neither ``data_list`` nor a valid ``column``
                is provided to the underlying :class:`ColumnsOperation` method.

        Example:
            Assuming ``users`` and ``admins`` tables::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', port=3306, username='root',
                            password='secret', db_name='my_app')
                users  = db.users
                admins = db.admins

                # Literal list
                cond1 = users.age.In([25, 30, 35])
                # cond1._output[0] -> '(`users`.`age` IN (%s, %s, %s))'
                # cond1._output[1] -> [25, 30, 35]

                # Subquery
                cond2 = users.name.In(
                    column=admins.username,
                    where=admins.active == True
                )

                # Use in a query
                result = users.get_row([users.name], where=cond2)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob.In(column=column, where=where, data_list=data_list)
    
    def not_In(self, column: Column|ColumnsOperation = None, where: ColumnsOperation = None, data_list: list = None):
        """Build an SQL ``NOT IN`` clause for the current column.

        This method serves as an entry point for the :class:`ColumnsOperation`
        ``not_In`` method. It initialises a :class:`ColumnsOperation` with the
        current column's name and delegates the execution to it, enabling
        seamless method chaining.

        Supports two modes:
        * Passing a list of literal values to ``data_list`` (or as the first
        positional argument for backward compatibility).
        * Passing a single :class:`Column`/:class:`ColumnsOperation` to
        ``column`` to build a ``SELECT`` subquery, with an optional
        ``where`` condition.

        Args:
            column: A single :class:`Column` or :class:`ColumnsOperation` to
                use in the ``SELECT`` clause of the subquery. If a list of
                literals is passed, it is treated as ``data_list``.
            where: An optional :class:`ColumnsOperation` (or :class:`Column`)
                representing the ``WHERE`` condition for the subquery.
            data_list: A list of literal values for a direct ``NOT IN`` clause.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the ``NOT IN`` clause, allowing further chaining.

        Raises:
            Exception: If neither ``data_list`` nor a valid ``column``
                is provided to the underlying :class:`ColumnsOperation` method.

        Example:
            Assuming ``users`` and ``admins`` tables::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', port=3306, username='root',
                            password='secret', db_name='my_app')
                users  = db.users
                admins = db.admins

                # Literal list
                cond1 = users.age.not_In([25, 30, 35])
                # cond1._output[0] -> '(`users`.`age` NOT IN (%s, %s, %s))'

                # Subquery
                cond2 = users.name.not_In(
                    column=admins.username,
                    where=admins.active == True
                )

                # Use in a query
                result = users.get_row([users.name], where=cond2)
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob.not_In(column=column, where=where, data_list=data_list)

    def If(self, condition):
        """
        Start a one-line conditional with this column as the "then" branch.

        Same behavior as :meth:`ColumnsOperation.If` but starts from the
        column itself. The generated SQL uses MySQL's ``IF(cond, then, else)``.

        Args:
            condition: A :class:`ColumnsOperation`, :class:`Column`, or raw
                value (auto-wrapped in :class:`LiteralValue`).

        Returns:
            _IfThenBuilder: Chain ``.Else(value)`` to complete.

        Example:
            >>> users.name.If(users.is_active == True).Else('inactive')
            # SQL: (IF((`users`.`is_active` = %s), `users`.`name`, %s))
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (self.name, [])
        return temp_ob.If(condition)


class BatchOperation:
    """
    Accumulate multiple SQL INSERT and UPDATE statements for batch execution.

    This class provides a mechanism to collect several database operations
    (inserts and updates) and execute them together in a single transaction.
    This is useful for improving performance by reducing round‑trips to the
    database and ensuring atomicity (all operations succeed or fail as a group).

    The class maintains an internal script list of queries and their associated
    parameters. Queries are added using the :meth:`update` and :meth:`insert`
    methods, and then executed with the :meth:`run` method. Each operation can
    involve complex :class:`ColumnsOperation` expressions, making it suitable
    for dynamic SQL generation.

    Attributes:
        script (list): A list where each element is a list or tuple representing
            a query and its parameters. For parameterized queries, the entry is
            ``[query_string, parameter_list]``. For queries without parameters,
            it is simply ``[query_string]``.
        table_obj (Table): The :class:`Table` instance associated with this
            batch operation. This is used to execute the queries via the
            underlying driver's ``_excs`` method.

    Example:
        Creating a batch operation with a mix of insert and update statements::

            from ormophine.Mysql import Table, Driver

            # Assume `db` is a Driver instance connected to a database
            users = db.users

            # Create a batch object
            batch = users.batch()

            # Add an update: increase age by 1 for users whose name starts with 'A'
            condition = users.name.startswith('A')
            batch.update(
                update={users.age: users.age + 1},
                where=condition
            )

            # Add an insert: create a new user
            batch.insert(
                insert={users.name: 'BatchUser', users.age: 25}
            )

            # Add another update using a ColumnsOperation (e.g., set email to full_name + '@domain.com')
            batch.update(
                update={users.email: users.first_name.add_end('@domain.com')},
                where=users.id > 100
            )

            # Execute all batched statements atomically
            batch.run()

        This ensures that either all three operations are applied to the
        database, or none are applied if an error occurs.
    """
    def __init__(self, table_object: Table):
        """
        Initialize a new BatchOperation instance.

        This constructor sets up a batch operation builder for the given table.
        The batch operation allows multiple SQL statements (e.g., updates and
        inserts) to be collected into a single script and executed together
        via the :meth:`run` method. The internal ``script`` list stores the
        queued operations.

        Args:
            table_object (Table): The :class:`Table` instance on which the
                batch operations will be performed. This table is used as
                the default target for operations unless overridden in the
                individual methods.

        Returns:
            None

        Example:
            Creating a batch operation for a table and adding updates::

                from ormophine.Mysql import Table, BatchOperation

                # Assume `users` is a Table instance
                batch = BatchOperation(users)
                batch.update({'age': users.age + 1}, where=users.id == 1)
                batch.insert({'name': 'Bob', 'age': 30})
                batch.run()  # Executes both statements in a single transaction
        """
        self.script = []
        self.table_obj = table_object

    def update(self, update: dict[Column, Any], where: ColumnsOperation, table: Table = None) -> 'BatchOperation':
        """
        Add an UPDATE operation to the batch script.

        This method appends an UPDATE statement to the internal batch script,
        which will be executed when :meth:`run` is called. The update can set
        column values to constants, other columns, or SQL expressions (via
        :class:`ColumnsOperation`). The WHERE clause is mandatory and must be
        a :class:`ColumnsOperation` expression.

        The method supports complex update expressions, such as arithmetic
        operations, string concatenations, and function calls, by accepting
        :class:`ColumnsOperation` objects as values in the ``update`` dict.

        Args:
            update (dict[Column, Any]): A dictionary mapping :class:`Column`
                objects to new values. Each value can be:
                - A constant (e.g., int, str, float) → uses a placeholder.
                - A :class:`Column` object → uses the column name directly.
                - A :class:`ColumnsOperation` object → uses its SQL fragment
                and merges its parameters.
            where (ColumnsOperation): A condition expression (e.g., using
                comparisons and logical operators) that determines which rows
                are updated.
            table (Table, optional): An alternative table to update. If not
                provided, the table associated with this :class:`BatchOperation`
                instance is used.

        Returns:
            BatchOperation: The same instance, allowing method chaining.

        Raises:
            Exception: If the generated SQL is invalid or the database operation
                fails during :meth:`run`. The error message includes the offending
                query and parameters.

        Example:
            Batch update with complex expressions::

                from ormophine.Mysql import BatchOperation, Table, Column

                # Assume `users` is a Table instance with columns: id, name, age, score
                batch = users.batch()

                # Update age to age + 1 and score to score * 2 for users older than 18
                condition = users.age > 18
                batch.update(
                    update={
                        users.age: users.age + 1,  # Arithmetic expression
                        users.score: users.score * 2,
                        users.name: users.name.add_end(' (updated)')  # String concatenation
                    },
                    where=condition
                )

                # Also update another table in the same batch
                # batch.update(..., table=another_table)

                batch.run()  # Execute all batched operations
        """
        if not update:
            return self
        temp_list= []
        [None if isinstance(value , Column) else temp_list.append(value) if not isinstance(value, ColumnsOperation) else temp_list.extend(value._output[1]) for key, value in update.items()]
        self.script.append([f'UPDATE {table.name_ if table else self.table_obj.name_} SET {', '.join(f'{key.first_name} = {value.first_name}' if isinstance(value , Column) else f'{key.first_name}=%s' if not isinstance(value , ColumnsOperation) else f'{key.first_name}={value._output[0]}' for key , value in list(update.items()))} WHERE {where._output[0]};', temp_list+where._output[1]])
        return self

    def insert(self, insert: dict[Column, Any], table: Table = None) -> 'BatchOperation':
        """
        Add an INSERT operation to the batch script.

        This method appends an INSERT statement to the internal batch script,
        which can later be executed as a single transaction using :meth:`run`.
        The operation will insert a single row into the target table. The columns
        and their corresponding values are provided as a dictionary mapping
        :class:`Column` objects to values. The table to insert into can be
        specified explicitly; if omitted, the table associated with this
        :class:`BatchOperation` instance is used.

        Args:
            insert (dict[Column, Any]): A dictionary where keys are :class:`Column`
                objects representing the columns to insert, and values are the
                corresponding values to insert. Values are added to the parameter
                list for safe SQL execution.
            table (Table, optional): The target :class:`Table` to insert into.
                If not provided, the table that was passed to the
                :class:`BatchOperation` constructor is used. Defaults to ``None``.

        Returns:
            BatchOperation: The same instance, with the INSERT operation appended
            to its internal script. This enables method chaining for building
            complex batch operations.

        Example:
            Building a batch insert for multiple rows::

                from ormophine.Mysql import BatchOperation

                # Assume `users` is a Table instance with columns: id, name, age
                batch = users.batch()

                # Insert a single row
                batch.insert({users.name: 'Alice', users.age: 30})

                # Insert another row, specifying a different table
                batch.insert({logs.message: 'User created'}, table=logs)

                # Execute all batched operations
                batch.run()
        """
        if not insert:
            self.script.append([f'INSERT INTO {self.table_obj.name_ if not table else table.name_} () VALUES ();', []])
            return self
        self.script.append([f'INSERT INTO {table.name_ if table else self.table_obj.name_} ({', '.join(i.first_name for i in list(insert.keys()))}) VALUES ({', '.join(f'%s' for k in insert)})' , [v for v in list(insert.values())]])
        return self

    def delete_row(self, where: ColumnsOperation, table: Table = None) -> 'BatchOperation':
        """
        Add a DELETE operation to the batch script.

        This method appends a DELETE statement to the internal batch script,
        which will be executed when :meth:`run` is called. The WHERE clause is
        mandatory and must be a :class:`ColumnsOperation` expression. Optionally,
        a different table can be specified as the target.

        Args:
            where (ColumnsOperation): A condition expression (e.g., using
                comparisons and logical operators) that determines which rows
                are deleted.
            table (Table, optional): An alternative table to delete from. If not
                provided, the table associated with this :class:`BatchOperation`
                instance is used.

        Returns:
            BatchOperation: The same instance, allowing method chaining.

        Raises:
            Exception: If the generated SQL is invalid or the database operation
                fails during :meth:`run`. The error message includes the offending
                query and parameters.

        Example:
            Batch delete with a condition::

                from ormophine.Mysql import BatchOperation

                batch = users.batch()

                # Delete users older than 60
                condition = users.age > 60
                batch.delete(where=condition)

                # Delete from another table in the same batch
                batch.delete(where=logs.timestamp < '2020-01-01', table=logs)

                batch.run()
        """
        self.script.append([f'DELETE FROM {table.name_ if table else self.table_obj.name_} WHERE {where._output[0]};', where._output[1]])
        return self

    def run(self):
        """
        Execute all batched SQL statements in the current batch operation.

        This method submits all accumulated queries (added via :meth:`update` and
        :meth:`insert`) to the database as a single transaction. The statements are
        executed sequentially, and if any statement fails, the entire transaction
        is rolled back. After execution, the internal script list is cleared.

        This method is typically the final step after building a batch with multiple
        :meth:`update` and :meth:`insert` calls. It is useful for reducing round‑trips
        to the database when performing multiple operations that should succeed or
        fail together.

        Args:
            None

        Returns:
            None

        Raises:
            Exception: If any SQL statement in the batch fails. The exception message
                includes the original error and the list of failed queries and their
                parameters for debugging. The transaction is rolled back on failure.

        Example:
            Performing a batch operation with an update that uses a complex
            :class:`ColumnsOperation` condition and multiple insertions::

                from ormophine.Mysql import Table, Driver, BatchOperation

                # Assume db is a Driver instance and users is a Table instance
                users = db.users

                # Build a batch operation
                batch = users.batch()

                # Add an update: set age = age + 1 for users whose name starts with 'A'
                condition = users.name.startswith('A')
                batch.update(
                    update={users.age: users.age + 1},
                    where=condition
                )

                # Add an insert: add a new user
                batch.insert(
                    insert={users.name: 'NewUser', users.age: 30}
                )

                # Add another update using a ColumnsOperation value (e.g., set email to full_name + '@domain.com')
                batch.update(
                    update={users.email: users.first_name.add_end('@domain.com')},
                    where=users.id > 100
                )

                # Execute all batched statements
                batch.run()
        """
        self.table_obj._excs(self.script)



from __future__ import annotations
from .. import Column, ColumnsOperation


class JoinQuery:
    """
    Fluent builder for MySQL/MariaDB JOIN queries with automatic table aliasing.

    ``JoinQuery`` is returned by :meth:`Table.inner_join`,
    :meth:`Table.left_join`, and :meth:`Table.right_join`. It accumulates
    join clauses lazily (without executing SQL) and lets the caller chain
    additional joins before finally materializing the result set through
    :meth:`get_row`.

    The builder is **immutable**: every call to :meth:`inner_join`,
    :meth:`left_join`, or :meth:`right_join` returns a *new*
    ``JoinQuery`` instance, leaving the original object unchanged. This
    makes it safe to reuse a partially-built query as a starting point for
    multiple downstream queries.

    Automatic table aliasing
    ------------------------
    If the same :class:`Table` object participates in a join more than
    once, a unique alias (e.g. ``orders_0``, ``orders_1``) is generated
    for each additional reference. All column references in subsequent
    ``ON``, ``WHERE``, ``SELECT`` and ``ORDER BY`` clauses are rewritten
    to use these aliases, so ambiguous column references are handled
    transparently.

    Identifier quoting
    ------------------
    MySQL/MariaDB use backticks (`` `table` ``) for identifier quoting,
    unlike PostgreSQL which uses double quotes. All generated SQL uses
    backticks consistently.

    Internal state
    --------------
    table_obj (Table): The base table (left side of the first join).
    joins (list[dict]): Accumulated join descriptors. Each dict contains
        the keys ``type`` (e.g. ``"INNER JOIN"``), ``table`` (a
        :class:`Table`), ``condition`` (a :class:`ColumnsOperation`), and
        ``alias`` (a string or ``None``).
    params (list): Bind parameters accumulated from all join conditions,
        kept in the order their placeholders appear in the generated SQL.
    _output (tuple[str, list]): A ``(sql_fragment, parameter_list)`` pair
        mirroring the shape of :class:`ColumnsOperation._output`, so a
        ``JoinQuery`` can be embedded in other expressions if needed.

    Example:
        Chain multiple joins and execute with ``get_row``::

            >>> users    = db.users_j
            >>> orders   = db.orders_j
            >>> products = db.products_j
            >>> rows = (users.inner_join(orders,   orders.user_id == users.id)
            ...             .inner_join(products, products.id == orders.product_id)
            ...             .get_row([users.username, products.name],
            ...                      where=products.price < 100,
            ...                      order_by=products.price * -1,
            ...                      limit=10))

        Reuse a partial query for two different downstream queries::

            >>> base = users.inner_join(orders, orders.user_id == users.id)
            >>> q1 = base.get_row([users.username])
            >>> q2 = base.get_row([orders.amount],
            ...                   where=orders.amount > 100)
            >>> # `base` is not mutated by either call.

    Note:
        - The condition passed to any ``*_join`` method must be a
          :class:`ColumnsOperation` (e.g. ``orders.user_id == users.id``).
          Passing a string or any other type raises an ``Exception``
          immediately, before any SQL is built.
        - Unlike :class:`Table.get_row`, ``JoinQuery.get_row`` always
          returns a list of tuples — even when only one column is
          selected — because the JOIN may involve multiple tables.
        - MySQL/MariaDB support ``RIGHT JOIN`` natively, but ``LEFT JOIN``
          is usually clearer and produces the same result when the tables
          are swapped.
    """
    def __init__(self, table_obj, joins=None, params=None):
        """Initialize a new :class:`JoinQuery` builder.

        This constructor is normally called indirectly through
        :meth:`Table.inner_join`, :meth:`Table.left_join`, or
        :meth:`Table.right_join`. It is also used internally by
        :meth:`_add_join` to produce a new immutable instance when a join is
        appended.

        Args:
            table_obj (Table): The base table on the left side of the first
                join. This table is used as the ``FROM`` clause of the final
                ``SELECT`` statement.
            joins (list[dict], optional): A list of previously accumulated
                join descriptors. Each item must be a dict with keys
                ``type``, ``table``, ``condition``, and ``alias``.
                Defaults to ``None`` (empty join list).
            params (list, optional): Bind parameters accumulated from
                previously added join conditions. Defaults to ``None``
                (empty parameter list).

        Returns:
            None: This method initializes the instance.

        Note:
            The ``_output`` attribute is computed immediately in the
            constructor. When ``joins`` is empty, ``_output`` is set to
            ``('', [])``. Otherwise it is set to ``(self._join_sql(),
            list(self.params))`` so that the resulting fragment is always
            consistent with the current accumulated state.
        """
        self.table_obj = table_obj
        self.joins = joins if joins is not None else []
        self.params = params if params is not None else []
        self._output = (self._join_sql(), list(self.params)) if self.joins else ('', [])

    def inner_join(self, table, condition, alias=None) -> 'JoinQuery':
        """Add an ``INNER JOIN`` clause and return a new :class:`JoinQuery`.

        An ``INNER JOIN`` only keeps rows where the ``ON`` condition is true
        on both sides. If the same ``table`` object has already been joined
        earlier in the chain, a unique alias is generated automatically and
        all column references inside ``condition`` (and any subsequent
        ``WHERE`` / ``ORDER BY`` / ``SELECT`` expressions) are rewritten to
        use that alias.

        Args:
            table (Table): The right-hand table to join.
            condition (ColumnsOperation): The ``ON`` condition, expressed as
                a :class:`ColumnsOperation` comparison (e.g.
                ``orders.user_id == users.id``). Passing a non-
                :class:`ColumnsOperation` value raises an exception
                immediately.
            alias (str, optional): An explicit alias for ``table``. When
                ``None`` (default), an alias is generated automatically only
                if the table has already been joined; otherwise the table is
                referenced by its own name.

        Returns:
            JoinQuery: A new :class:`JoinQuery` instance with the
            ``INNER JOIN`` clause appended. The original instance is not
            modified.

        Raises:
            Exception: If ``condition`` is not a :class:`ColumnsOperation`.

        Example:
            Single inner join::

                >>> users  = db.users_j
                >>> orders = db.orders_j
                >>> rows = (users.inner_join(orders, orders.user_id == users.id)
                ...             .get_row([users.username, orders.amount]))

            Chained inner joins::

                >>> products = db.products_j
                >>> rows = (users.inner_join(orders,   orders.user_id == users.id)
                ...             .inner_join(products, products.id == orders.product_id)
                ...             .get_row([users.username, products.name]))
        """
        return self._add_join('INNER JOIN', table, condition, alias)

    def left_join(self, table, condition, alias=None) -> 'JoinQuery':
        """Add a ``LEFT JOIN`` clause and return a new :class:`JoinQuery`.

        A ``LEFT JOIN`` keeps all rows from the base (left) table and fills
        columns of the joined table with ``NULL`` when no matching row
        exists. Automatic aliasing and column rewriting behave exactly like
        :meth:`inner_join`.

        Args:
            table (Table): The right-hand table to join.
            condition (ColumnsOperation): The ``ON`` condition (e.g.
                ``orders.user_id == users.id``).
            alias (str, optional): An explicit alias for ``table``. When
                ``None`` (default), an alias is generated automatically if
                the table has already been joined.

        Returns:
            JoinQuery: A new :class:`JoinQuery` instance with the ``LEFT
            JOIN`` clause appended.

        Raises:
            Exception: If ``condition`` is not a :class:`ColumnsOperation`.

        Example:
            Keep unmatched users (with ``NULL`` amounts)::

                >>> rows = (users.left_join(orders, orders.user_id == users.id)
                ...             .get_row([users.username, orders.amount]))
                >>> # Users without orders appear with `None` amount.

            Filter only the unmatched rows::

                >>> rows = (users.left_join(orders, orders.user_id == users.id)
                ...             .get_row([users.username, orders.amount],
                ...                      where=orders.amount == None))
        """
        return self._add_join('LEFT JOIN', table, condition, alias)

    def right_join(self, table, condition, alias=None) -> 'JoinQuery':
        """Add a ``RIGHT JOIN`` clause and return a new :class:`JoinQuery`.

        A ``RIGHT JOIN`` keeps all rows from the joined (right) table and
        fills columns of the base table with ``NULL`` when no matching row
        exists. Automatic aliasing and column rewriting behave exactly like
        :meth:`inner_join`.

        Args:
            table (Table): The right-hand table to join.
            condition (ColumnsOperation): The ``ON`` condition (e.g.
                ``orders.user_id == users.id``).
            alias (str, optional): An explicit alias for ``table``. When
                ``None`` (default), an alias is generated automatically if
                the table has already been joined.

        Returns:
            JoinQuery: A new :class:`JoinQuery` instance with the ``RIGHT
            JOIN`` clause appended.

        Raises:
            Exception: If ``condition`` is not a :class:`ColumnsOperation`.

        Example:
            Keep unmatched orders (with ``NULL`` usernames)::

                >>> rows = (users.right_join(orders, orders.user_id == users.id)
                ...             .get_row([users.username, orders.amount]))

        Note:
            Both MySQL and MariaDB support ``RIGHT JOIN`` natively. In
            practice, ``LEFT JOIN`` is more common; swapping the two tables
            and using :meth:`left_join` produces the same result and is
            often clearer.
        """
        return self._add_join('RIGHT JOIN', table, condition, alias)

    def _make_unique_alias(self, table) -> str:
        """Generate a unique alias for a table within this join chain.

        Called internally when the same :class:`Table` object is joined more
        than once. The generated alias follows the pattern
        ``<table_name>_<counter>`` (e.g. ``orders_0``, ``orders_1``), where
        ``counter`` starts at 0 and increases until an unused alias is found.

        Args:
            table (Table): The table for which an alias is needed.

        Returns:
            str: A unique alias string that does not collide with any alias
            already used by the joins accumulated on this instance.

        Example:
            Internal usage::

                >>> # First join of `orders` uses no alias,
                >>> # second join gets 'orders_0', third gets 'orders_1', ...
                >>> alias = self._make_unique_alias(orders)

        Note:
            The surrounding backticks of the table name are stripped before
            the counter suffix is appended; the alias is later re-quoted
            with backticks when it is emitted in the SQL.
        """
        base = table.name_[1:-1]  
        used = {j['alias'] for j in self.joins if j['alias']}
        i = 0
        while f'{base}_{i}' in used:
            i += 1
        return f'{base}_{i}'

    def _add_join(self, join_type, table, condition, alias):
        """Append a join descriptor to the chain and return a new instance.

        This is the shared implementation used by :meth:`inner_join`,
        :meth:`left_join`, and :meth:`right_join`. It validates the
        condition type, auto-generates an alias if the table is already
        joined, and produces a new immutable :class:`JoinQuery` containing
        the extended join list and parameter list.

        Args:
            join_type (str): The SQL join keyword, e.g. ``"INNER JOIN"``,
                ``"LEFT JOIN"``, or ``"RIGHT JOIN"``.
            table (Table): The right-hand table being joined.
            condition (ColumnsOperation): The ``ON`` condition.
            alias (str or None): An explicit alias for ``table``, or ``None``
                to let the method decide (auto-alias only if the table is
                already present in ``self.joins``).

        Returns:
            JoinQuery: A new :class:`JoinQuery` instance whose ``joins``
            list is ``self.joins + [new_descriptor]`` and whose ``params``
            list is extended with the parameters of ``condition``.

        Raises:
            Exception: If ``condition`` is not a
                :class:`ColumnsOperation`.

        Note:
            This method does **not** mutate the current instance. Each call
            returns a brand-new ``JoinQuery``, which is what makes the
            builder safe to reuse.
        """
        if not isinstance(condition, ColumnsOperation):
            raise Exception(
                f"Join condition must be a ColumnsOperation, got "
                f"{type(condition).__name__}. Build it with column comparisons "
                f"like `table1.col == table2.col`."
            )

        already_joined = any(j['table'] is table for j in self.joins)
        if alias is None and already_joined:
            alias = self._make_unique_alias(table)

        new_joins = self.joins + [{
            'type': join_type,
            'table': table,
            'condition': condition,
            'alias': alias,
        }]
        new_params = self.params + list(condition._output[1])
        return JoinQuery(self.table_obj, new_joins, new_params)

    def _join_sql(self) -> str:
        """Build the SQL fragment for all accumulated join clauses.

        Iterates over ``self.joins`` in insertion order and produces a single
        string of the form::

            INNER JOIN `orders` ON (...)
            LEFT  JOIN `products` AS `products_0` ON (...)
            ...

        When a join descriptor carries an alias, all occurrences of the
        table's fully qualified prefix (e.g. `` `orders`. ``) inside its
        condition are rewritten to the alias prefix (e.g. `` `orders_0`. ``)
        before the fragment is emitted.

        Returns:
            str: The concatenated join SQL fragment, ready to be inserted
            after the ``FROM <base_table>`` clause. Returns an empty string
            when no joins have been added.

        Example:
            Internal usage::

                >>> jq = users.inner_join(orders, orders.user_id == users.id)
                >>> jq._join_sql()
                'INNER JOIN `orders` ON (`orders`.`user_id` = `users`.`id`)'
        """
        parts = []
        for j in self.joins:
            tbl = j['table']
            cond_sql = j['condition']._output[0]
            if j['alias']:
                cond_sql = cond_sql.replace(
                    f'{tbl.name_}.', f'`{j["alias"]}`.'
                )
                parts.append(
                    f'{j["type"]} {tbl.name_} AS `{j["alias"]}` ON {cond_sql}'
                )
            else:
                parts.append(f'{j["type"]} {tbl.name_} ON {cond_sql}')
        return ' '.join(parts)

    def _resolve_column_ref(self, col) -> str:
        """Return the SQL reference for a column, using its join alias if any.

        Given a :class:`Column`, this helper scans the accumulated joins and,
        if the column's parent table appears with an alias, rewrites the
        reference to use that alias. This is necessary when the same table
        has been joined more than once and the plain ``table.column`` form
        would be ambiguous.

        Args:
            col (Column): The column whose SQL reference is needed.

        Returns:
            str: Either the plain qualified name (e.g. ``'`users`.`id`'``)
            or the aliased form (e.g. ``'`users_0`.`id`'``) when the parent
            table is aliased.

        Example:
            Internal usage::

                >>> ref = jq._resolve_column_ref(users.id)
                >>> ref
                '`users`.`id`'
        """
        for j in self.joins:
            if j['table'] is col.table_obj:
                if j['alias']:
                    return f'`{j["alias"]}`.`{col.first_name[1:-1]}`'
                return col.name
        return col.name

    def _rewrite_expr(self, expr: str) -> str:
        """Rewrite table references inside a SQL expression to use aliases.

        This helper walks through the accumulated joins and, for each join
        that carries an alias, replaces occurrences of the table's fully
        qualified prefix (e.g. `` `orders`. ``) with the alias prefix (e.g.
        `` `orders_0`. ``). It is used to preprocess strings produced by
        :class:`ColumnsOperation` before they are placed in the final SQL.

        Args:
            expr (str): A SQL fragment that may contain table-qualified
                column references.

        Returns:
            str: The same fragment with aliased table references.

        Example:
            Internal usage::

                >>> jq._rewrite_expr('(`orders`.`total` * %s)')
                '(`orders_0`.`total` * %s)'
        """
        for j in self.joins:
            if j['alias']:
                expr = expr.replace(
                    f'{j["table"].name_}.', f'`{j["alias"]}`.'
                )
        return expr

    def get_row(
        self,
        which_columns: list,
        where: 'ColumnsOperation' = None,
        order_by: 'Column | ColumnsOperation' = None,
        limit: int = None,
        offset: int = None,
    ):
        """Execute the accumulated JOIN query and return the fetched rows.

        Builds and runs a ``SELECT ... FROM base_table <joins> [WHERE ...]
        [ORDER BY ...] [LIMIT ...] [OFFSET ...]`` statement using the joins
        accumulated on this :class:`JoinQuery` instance. Column references
        inside ``which_columns``, ``where`` and ``order_by`` are rewritten
        to use table aliases when a table participates in the join more than
        once, preventing ambiguous-column errors.

        Args:
            which_columns (list[Column | ColumnsOperation]): The columns or
                computed expressions to include in the ``SELECT`` list. Each
                element can be a :class:`Column` (returned as-is with an
                alias of the form ``<table>_<column>``) or a
                :class:`ColumnsOperation` (e.g. ``orders.total * -1``,
                ``users.username.upper()``).
            where (ColumnsOperation, optional): A filter condition. Both
                sides may reference columns from any table participating in
                the join; aliases are applied automatically. Defaults to
                ``None`` (no filter).
            order_by (Column | ColumnsOperation, optional): The expression to
                sort by. May be a plain :class:`Column` (e.g. ``users.id``)
                or a computed :class:`ColumnsOperation` (e.g.
                ``orders.total * -1``, ``users.username.upper()``).
                Descending order is expressed by multiplying the column by
                ``-1``, since the ORM has no ``DESC`` keyword. Defaults to
                ``None`` (no ordering).
            limit (int, optional): Maximum number of rows to return. A value
                of ``0`` returns an empty list. Defaults to ``None`` (no
                limit).
            offset (int, optional): Number of rows to skip before returning
                results. When greater than the total row count, returns an
                empty list. MySQL requires ``LIMIT`` before ``OFFSET``; when
                ``offset`` is given without ``limit``, the method emits
                ``LIMIT 18446744073709551615`` — the largest possible 64-bit
                unsigned integer, which MySQL treats as "no limit".
                Defaults to ``None`` (no offset).

        Returns:
            list[tuple]: A list of tuples, one per returned row, where each
            tuple contains the values in the order of ``which_columns``.
            Always returns tuples (never a flat list), even when only one
            column is requested, because the join may span several tables.

        Raises:
            Exception: Propagates any database errors
                (:class:`OperationalError`, :class:`ProgrammingError`, etc.)
                with additional context about the failing query.

        Example:
            Basic inner join::

                >>> users  = db.users_j
                >>> orders = db.orders_j
                >>> rows = (users.inner_join(orders, orders.user_id == users.id)
                ...             .get_row([users.username, orders.amount]))
                >>> # [('alice', 1000), ('alice', 20), ('bob', 50)]

            Chained joins with a computed ``ORDER BY``::

                >>> products = db.products_j
                >>> rows = (users.inner_join(orders,   orders.user_id == users.id)
                ...             .inner_join(products, products.id == orders.product_id)
                ...             .get_row([users.username, products.name],
                ...                      where=products.price < 100,
                ...                      order_by=products.price * -1,
                ...                      limit=2,
                ...                      offset=1))

            Case-insensitive ordering using a string operation::

                >>> rows = (users.inner_join(orders, orders.user_id == users.id)
                ...             .get_row([users.id],
                ...                      order_by=users.username.upper()))

            Full stack: filter + computed order + pagination::

                >>> rows = (users.inner_join(orders, orders.user_id == users.id)
                ...             .get_row([orders.amount],
                ...                      where=orders.amount > 20,
                ...                      order_by=orders.amount * -1 + 1000,
                ...                      limit=2,
                ...                      offset=1))

            Offset without limit (uses MySQL's ``LIMIT 18446744073709551615``
            idiom internally)::

                >>> rows = (users.inner_join(orders, orders.user_id == users.id)
                ...             .get_row([orders.amount],
                ...                      order_by=orders.amount * -1,
                ...                      offset=7))
        """
        if not which_columns:
            return []
        tl = []
        select_parts = []
        for i in which_columns:
            if isinstance(i, Column):
                ref = self._resolve_column_ref(i)
                alias = f'{i.table_obj.name_[1:-1]}_{i.first_name[1:-1]}'
                select_parts.append(f'{ref} AS {alias}')
            else:  # ColumnsOperation
                expr = self._rewrite_expr(i._output[0])
                tl.extend(i._output[1])
                if expr.startswith('(') and expr.endswith(')'):
                    expr = expr[1:-1]
                alias = (
                    f'{i.col_obj.table_obj.name_[1:-1]}_'
                    f'{i.col_obj.first_name[1:-1]}'
                )
                select_parts.append(f'{expr} AS {alias}')
        ob = []
        if isinstance(order_by, Column):
            order_sql = self._resolve_column_ref(order_by)
        elif isinstance(order_by, ColumnsOperation):
            order_sql = self._rewrite_expr(order_by._output[0])
            ob.extend(order_by._output[1])
        else:
            order_sql = None
        sql = (
            f"SELECT {', '.join(select_parts)} "
            f"FROM {self.table_obj.name_} "
            f"{self._join_sql()}"
        )
        all_params = tl + list(self.params)
        if where is not None:
            sql += f' WHERE {self._rewrite_expr(where._output[0])}'
            all_params += list(where._output[1])
        if order_sql is not None:
            sql += f' ORDER BY {order_sql}'
            all_params += ob
        if limit is not None:
            sql += ' LIMIT %s'
            all_params.append(limit)
        if offset is not None:
            if limit is None:
                sql += ' LIMIT 18446744073709551615'
            sql += ' OFFSET %s'
            all_params.append(offset)

        sql += ';'

        rows = self.table_obj._excfp(sql, all_params) if all_params else self.table_obj._excf(sql)
        return rows

from __future__ import annotations
from .. import Column, ColumnsOperation, BatchOperation, JoinQuery
from typing import Any

class Table:
    """
    Represents a database table and provides an ORM-like interface for operations.

    The :class:`Table` class is the primary interface for interacting with a
    specific database table. It is typically created automatically by the
    :class:`Driver` when a connection is established, and each table in the
    database becomes an attribute of the driver instance (e.g., ``db.users``).

    Each :class:`Table` instance dynamically creates :class:`Column` attributes
    for every column in the table, allowing you to reference columns as
    attributes (e.g., ``users.id``, ``users.name``). These columns can be used
    in queries, comparisons, and operations.

    The class provides a full suite of methods for:
        - Inserting, updating, deleting rows.
        - Querying with optional filtering and ordering.
        - Batch operations for performance.
        - Joining tables.
        - Index management.
        - Schema modification (adding/dropping columns, renaming tables/columns).
        - Executing custom SQL.

    **Placeholder Mechanism for Bulk Updates**
        The :attr:`PLACE_HOLDER` attribute (a unique string) is used internally
        in :meth:`bulk_update` to mark positions where values from the data list
        should be substituted. If you need to change this placeholder, you can
        override it on a per-table basis::

            table.PLACE_HOLDER = 'MY_CUSTOM_PLACEHOLDER'

        However, ensure that this string does not appear in your actual data,
        as it is used for string replacement.

    Attributes:
        name_ (str): The table name wrapped in backticks (e.g., ``'`users`'``).
        db_obj (Driver): The parent driver instance that owns this table.
        PLACE_HOLDER (str): A unique placeholder string used in bulk updates
            (default: ``'_MY_S4ULT3D_PL4C3_H0LD3R_%s_'``).
        <column_name> (Column): For each column in the table, a :class:`Column`
            attribute is dynamically created (e.g., ``table.id``, ``table.name``).

    Example:
        Assuming a driver instance ``db`` connected to a database with a
        ``users`` table::

            # Access the table
            users = db.users

            # Insert a new user
            users.insert({users.name: 'Alice', users.age: 30})

            # Update a user
            users.update(
                {users.age: 31},
                where=users.name == 'Alice'
            )

            # Query rows
            rows = users.get_row(
                which_columns=[users.name, users.age],
                where=users.age >= 18,
                order_by=users.name
            )
            # rows is a list of tuples: [('Alice', 31), ('Bob', 25), ...]

            # Batch insert
            users.bulk_insert(
                columns=[users.name, users.age],
                data_list=[['Charlie', 40], ['Dave', 22]]
            )

            # Delete rows
            users.delete_row(where=users.age > 100)

        For more advanced operations like joins and batch updates, refer to the
        individual method documentation.
    """

    class _PlaceHolder:
        """Marker object used in :meth:`Table.bulk_update` to inject row values.

        When building a bulk update such as ``employees.salary + employees.PLACE_HOLDER``,
        the placeholder tells :class:`ColumnsOperation` to treat the operand as numeric
        (not string), avoiding ambiguity between ``+`` and ``||``. During
        :meth:`Table.bulk_update`, each placeholder is replaced by a fresh ``%s`` and
        bound to the matching value from ``data_list``. Users normally interact with
        ``table.PLACE_HOLDER``; the literal string can be changed per-table if it
        collides with application data:
        ``my_table.PLACE_HOLDER = "your_own_marker"``.
        """
        def __init__(self, placeholder):
            self.placeholder = placeholder
        
        def __str__(self):
            return self.placeholder
        
    def __init__(self, obj: Driver, table_name: str):
        """
        Initialize a Table instance representing a database table.

        This constructor stores the provided :class:`Driver` instance and the
        table name. It then retrieves the table's schema information using
        :meth:`get_table_info`, and for each column, dynamically creates an
        attribute on the instance. The attribute name is the column name, and
        its value is a :class:`Column` object representing that column.

        Args:
            obj (Driver): The driver instance managing the database connection
                and connection pool. This is used to execute queries against
                the table.
            table_name (str): The name of the database table. This will be
                quoted as an identifier (surrounded by backticks) internally.

        Returns:
            None

        Raises:
            Exception: If the table does not exist or the database query fails
                (propagated from :meth:`get_table_info`). The exception message
                will include the original error and context.

        Example:
            Assuming a configured :class:`Driver` instance ``db`` connected to
            a database containing a table named ``users``::

                # The Table instance is automatically created when the driver
                # loads existing tables, but you can also create one manually:
                users_table = Table(db, 'users')

                # Access columns as attributes:
                users_table.id           # Column object for 'id'
                users_table.username     # Column object for 'username'

                # The instance is also added as an attribute of the driver:
                # db.users is the same object if the table existed at driver init.
        """
        self.name_= '`'+table_name+'`'
        self.db_obj = obj
        self.PLACE_HOLDER = self._PlaceHolder('_MY_S4ULT3D_PL4C3_H0LD3R_%s_')
        for i in self.get_table_info():
            self.__setattr__(i['name'], Column(self, i['name'], i['datatype']))

    def _exc(self, query):
        """
        Execute a SQL query without parameters and commit the transaction.

        This is an internal wrapper method that delegates the execution to the
        underlying :class:`Driver` instance's :meth:`~Driver._exc` method.
        It is used for queries that do not require parameter substitution, such as
        ``CREATE TABLE``, ``DROP TABLE``, or ``ALTER TABLE`` statements. The
        transaction is automatically committed upon successful execution.

        Args:
            query (str): The SQL query string to execute. Must not contain
                placeholders (``%s``), as no parameters are provided.

        Returns:
            None

        Raises:
            Exception: If an operational or programming error occurs during
                execution. The exception is propagated from the driver layer
                and includes details about the query and the original error.

        Example:
            Renaming a column using the internal method::

                # Assuming `users` is a Table instance
                users._exc("ALTER TABLE `users` ADD COLUMN `age` INT;")
        """
        self.db_obj._exc(query)

    def _excp(self, query, params):
        """
        Execute a parameterized SQL query on the table's database connection.

        This internal method delegates to the underlying :class:`Driver` instance's
        ``_excp`` method, which handles connection pooling, transaction management,
        and error recovery. It is intended for use by other :class:`Table` methods
        that require parameterized queries.

        Args:
            query (str): The SQL query string containing placeholders (``%s``) for
                parameters.
            params (list or tuple): The parameter values to substitute into the query.

        Returns:
            None

        Raises:
            Exception: If an operational or programming error occurs during query
                execution. The exception message will include the original error,
                the query, and the parameters to aid debugging. This method
                propagates any exceptions raised by the driver's ``_excp`` method.

        Example:
            This method is typically used internally, but can be called directly
            for executing custom parameterized queries on a specific table::

                # Assuming `users` is a Table instance
                users._excp(
                    "UPDATE users SET age = %s WHERE id = %s",
                    (25, 1)
                )
        """
        self.db_obj._excp(query, params)

    def _excf(self, query):
        """
        Execute a query without parameters and fetch all results.

        This internal method delegates to the underlying :class:`Driver`'s
        ``_excf`` method, which retrieves a connection from the pool, executes
        the provided query, commits the transaction, and returns the fetched
        rows. It is used internally by various :class:`Table` methods that
        need to retrieve data without parameterized queries.

        Args:
            query (str): The SQL query string to execute. Must not contain
                parameter placeholders.

        Returns:
            list of tuple: A list of rows, where each row is a tuple of column
                values. The structure depends on the query.

        Raises:
            Exception: If the query execution fails (e.g., syntax error,
                connection issue, or other database error). The original error
                and query are included in the exception message.

        Example:
            This method is typically used internally, not directly by user code.
            However, it can be used for custom queries::

                table = db.users
                result = table._excf("SELECT id, name FROM users WHERE active = 1")
                for row in result:
                    print(row)  # e.g., (1, 'Alice')
        """
        return self.db_obj._excf(query)

    def _excfp(self, query, params):
        """
        Execute a parameterized query and fetch all results.

        This internal method delegates to the underlying :class:`Driver`'s
        ``_excfp`` method, which retrieves a connection from the pool, executes
        the provided query with the given parameters, commits the transaction,
        and returns the fetched rows. It is used internally by various
        :class:`Table` methods that need to retrieve data with parameterized
        queries.

        Args:
            query (str): The SQL query string containing ``%s`` placeholders
                for parameters.
            params (list or tuple): The parameter values to substitute into
                the query. Must match the number and order of placeholders.

        Returns:
            list of tuple: A list of rows, where each row is a tuple of column
                values. The structure depends on the query.

        Raises:
            Exception: If the query execution fails (e.g., syntax error,
                parameter mismatch, connection issue, or other database error).
                The original error and query/params are included in the exception
                message.

        Example:
            This method is typically used internally, not directly by user code.
            However, it can be used for custom parameterized queries::

                table = db.users
                result = table._excfp(
                    "SELECT id, name FROM users WHERE age > %s",
                    (18,)
                )
                for row in result:
                    print(row)  # e.g., (1, 'Alice')
        """
        return self.db_obj._excfp(query, params)

    def _excm(self, query, params):
        """
        Execute a parameterized query multiple times with different parameter sets.

        This internal method delegates to the underlying :class:`Driver`'s
        ``_excm`` method, which retrieves a connection from the pool, executes
        the provided query once for each parameter set in the list using
        ``cursor.executemany()``, commits the transaction, and returns the
        connection to the pool. It is typically used for bulk operations like
        :meth:`bulk_insert` and :meth:`bulk_update`.

        Args:
            query (str): The SQL query string containing placeholders (``%s``) for
                parameters.
            params (list of tuple or list of list): A sequence of parameter sets,
                where each set contains the values to substitute into the query
                for one execution. The number of elements in each set must match
                the number of placeholders in the query.

        Returns:
            None

        Raises:
            Exception: If the query execution fails (e.g., syntax error, data type
                mismatch, connection issue, or other database error). The original
                error, query, and parameters are included in the exception message
                to aid debugging.

        Example:
            This method is typically used internally by bulk operations.
            For example, to insert multiple rows::

                table = db.users
                query = "INSERT INTO users (name, age) VALUES (%s, %s)"
                params = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
                table._excm(query, params)
                # All three rows are inserted in a single round-trip.
        """
        self.db_obj._excm(query, params)

    def _excs(self, query_params: list):
        """
        Execute a batch of SQL statements with optional parameters.

        This internal method delegates to the underlying :class:`Driver`'s
        ``_excs`` method, which processes multiple SQL queries sequentially
        within a single transaction. Each query in the batch can optionally
        include parameter placeholders. The method is used internally by
        :class:`BatchOperation` to execute batched updates or inserts.

        Args:
            query_params (list): A list where each element is either:

                - A list or tuple of exactly two elements: ``[query, params]``,
                where ``query`` is a SQL string with placeholders and ``params``
                is a list/tuple of parameter values.
                - A single string (or a list with one element) representing a
                query without parameters.

        Returns:
            None

        Raises:
            Exception: If any query in the batch fails. The exception message
                includes the original error and a list of all queries and their
                parameters for debugging. The transaction is rolled back on failure.

        Example:
            This method is typically used internally, but can be called for
            batch execution::

                table = db.users
                queries = [
                    ["UPDATE users SET active = 1 WHERE id = %s", [1]],
                    ["UPDATE users SET active = 1 WHERE id = %s", [2]],
                ]
                table._excs(queries)
        """
        self.db_obj._excs(query_params)

    def get_columns_name(self):
        """
        Retrieve the names of all columns in the table.

        This method executes a ``SHOW COLUMNS`` query and extracts the column names
        from the result set. It is a convenient way to get a list of column
        identifiers without fetching the full schema information.

        Returns:
            list of str: A list of column names as strings, in the order they
            appear in the table definition.

        Raises:
            Exception: If the underlying query fails (e.g., the table does not
                exist, connection issues, or permission problems). The original
                error and the query are included in the exception message.

        Example:
            Assuming a ``Table`` instance ``users`` exists::

                columns = users.get_columns_name()
                print(columns)  # e.g., ['id', 'name', 'email', 'created_at']
        """
        return [i[0] for i in self._excf(f'SHOW COLUMNS FROM {self.name_}')]
    
    def get_table_info(self):
        """
        Retrieve complete metadata information about all columns in the table.

        This method queries the MySQL ``INFORMATION_SCHEMA`` database to obtain
        detailed column information, including data type, nullability, default
        values, primary key status, foreign key relationships, and other
        attributes. The returned data is similar in structure to SQLite's
        ``PRAGMA table_info`` but with additional MySQL‑specific fields.

        The method is called during :class:`Table` initialization to dynamically
        create :class:`Column` attributes for each table column.

        Returns:
            list of dict: A list where each dictionary represents a column and
            contains the following keys:

            - ``cid`` (int): Column ordinal position (1‑based).
            - ``name`` (str): Column name.
            - ``type`` (str): MySQL data type name (e.g., ``'int'``, ``'varchar'``).
            - ``datatype`` (type): Python type mapping (``int``, ``float``, ``str``,
            or ``bytes``) inferred from the MySQL type.
            - ``notnull`` (bool): ``True`` if the column is ``NOT NULL``.
            - ``dflt_value`` (Any): Default value for the column, or ``None``.
            - ``pk`` (bool): ``True`` if the column is part of the primary key.
            - ``full_type`` (str): Complete column type definition (e.g.,
            ``'int(11)'``, ``'varchar(255)'``).
            - ``extra`` (str): Additional information (e.g., ``'auto_increment'``).
            - ``charset`` (str): Character set name, or ``None``.
            - ``collation`` (str): Collation name, or ``None``.
            - ``numeric_precision`` (int): Numeric precision for numeric types.
            - ``numeric_scale`` (int): Numeric scale for numeric types.
            - ``datetime_precision`` (int): Fractional seconds precision for
            temporal types.
            - ``auto_increment`` (bool): ``True`` if the column has
            ``AUTO_INCREMENT``.
            - ``fk_table`` (str): Referenced table name for foreign keys, or
            ``None``.
            - ``fk_column`` (str): Referenced column name for foreign keys, or
            ``None``.
            - ``fk_on_update`` (str): ``ON UPDATE`` action for foreign key, or
            ``None``.
            - ``fk_on_delete`` (str): ``ON DELETE`` action for foreign key, or
            ``None``.

        Raises:
            Exception: If the underlying database query fails (e.g., connection
                issue, table does not exist). The original error and query are
                included in the exception message.

        Example:
            Retrieving column information for a table::

                db = Driver(...)
                table = db.users
                info = table.get_table_info()
                for col in info:
                    print(f"{col['name']} ({col['type']}) PK: {col['pk']}")
                # Example output:
                # id (int) PK: True
                # name (varchar) PK: False
        """
        
        query = f"""
            SELECT 
                c.ORDINAL_POSITION AS cid,
                c.COLUMN_NAME AS name,
                c.DATA_TYPE AS type,
                CASE WHEN c.IS_NULLABLE = 'NO' THEN 1 ELSE 0 END AS notnull,
                c.COLUMN_DEFAULT AS dflt_value,
                CASE WHEN c.COLUMN_KEY = 'PRI' THEN 1 ELSE 0 END AS pk,
                c.COLUMN_TYPE AS full_type,
                c.EXTRA AS extra,
                c.CHARACTER_SET_NAME AS charset,
                c.COLLATION_NAME AS collation,
                c.NUMERIC_PRECISION AS num_precision,
                c.NUMERIC_SCALE AS num_scale,
                c.DATETIME_PRECISION AS datetime_precision,
                CASE WHEN c.EXTRA LIKE '%%auto_increment%%' THEN 1 ELSE 0 END AS auto_increment,
                kcu.REFERENCED_TABLE_NAME AS fk_table,
                kcu.REFERENCED_COLUMN_NAME AS fk_column,
                rc.UPDATE_RULE AS fk_on_update,
                rc.DELETE_RULE AS fk_on_delete
            FROM INFORMATION_SCHEMA.COLUMNS c
            LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
                ON kcu.TABLE_SCHEMA = c.TABLE_SCHEMA
                AND kcu.TABLE_NAME = c.TABLE_NAME
                AND kcu.COLUMN_NAME = c.COLUMN_NAME
                AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
            LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
                ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
                AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
                AND rc.TABLE_NAME = c.TABLE_NAME
            WHERE c.TABLE_SCHEMA = DATABASE()
            AND c.TABLE_NAME = %s
            ORDER BY c.ORDINAL_POSITION
        """
        return [{
            'cid': row[0],           # ORDINAL_POSITION
            'name': row[1],          # COLUMN_NAME
            'type': row[2],          # DATA_TYPE (MySQL type name)
            'datatype': (int if row[2].lower().split('(')[0] in ('int', 'integer', 'tinyint', 'smallint', 'mediumint', 'bigint', 'serial', 'year', 'bit') else float if row[2].lower().split('(')[0] in ('real', 'float', 'double', 'decimal', 'numeric') else  bool if row[2].lower().split('(')[0] == 'boolean' else object if row[2].lower().split('(')[0] in ('date', 'time', 'datetime', 'timestamp')else str if row[2].lower().split('(')[0] in ('char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext', 'enum', 'set', 'json')else bytes if row[2].lower().split('(')[0] in ('blob', 'tinyblob', 'mediumblob', 'longblob', 'binary', 'varbinary', 'geometry', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection') else str),
            'notnull': bool(row[3]), # True/False
            'dflt_value': row[4],    # COLUMN_DEFAULT
            'pk': bool(row[5]),      # True/False
            'full_type': row[6],     # COLUMN_TYPE (مثلاً 'int(11)')
            'extra': row[7],         # EXTRA (auto_increment, etc.)
            'charset': row[8],       # CHARACTER_SET_NAME
            'collation': row[9],     # COLLATION_NAME
            'numeric_precision': row[10],  # NUMERIC_PRECISION
            'numeric_scale': row[11],      # NUMERIC_SCALE
            'datetime_precision': row[12], # DATETIME_PRECISION
            'auto_increment': bool(row[13]),  # True/False
            'fk_table': row[14],     # REFERENCED_TABLE_NAME
            'fk_column': row[15],    # REFERENCED_COLUMN_NAME
            'fk_on_update': row[16], # UPDATE_RULE
            'fk_on_delete': row[17]  # DELETE_RULE
        } for row in self._excfp(query, (self.name_[1:-1],))]

    def batch(self) -> 'BatchOperation':
        """
        Create a new batch operation builder for this table.

        Batch operations allow multiple ``UPDATE`` and ``INSERT`` statements to be
        queued together and executed in a single transaction. This is useful for
        performing multiple related changes efficiently or for constructing dynamic
        scripts where each statement depends on previous data. The returned
        :class:`BatchOperation` object provides chainable ``update()`` and
        ``insert()`` methods to build the batch, and a ``run()`` method to execute
        all queued statements.

        Returns:
            BatchOperation: A new batch operation instance bound to this table.

        Example (Simple)::
            # Simple batch with one update and one insert
            table = db.users

            (table.batch()
                .update({'age': 30}, table.age < 18)
                .insert({'name': 'John', 'age': 25})
                .run())

        Example (Complex using ColumnsOperation)::
            # Complex batch with arithmetic and string operations
            table = db.products
            batch = table.batch()

            # Increase price by 10% for products with low stock
            batch.update(
                {table.price: table.price * 1.10},
                table.stock < 5
            )

            # Set description to concatenate name and category, with uppercase
            batch.update(
                {table.description: table.name.add_end(' - ').add_end(table.category).upper()},
                table.description == ''
            )

            # Insert a new product with a computed value
            batch.insert({
                table.name: 'Premium Item',
                table.price: table.price + 20,
                table.stock: 100
            })

            # Execute all statements in one transaction
            batch.run()
        """
        return BatchOperation(self)

    def update(self, update: dict[Column, Any], where: 'ColumnsOperation') -> None:
        """
        Update rows in the table that match the given condition.

        This method constructs and executes an ``UPDATE`` SQL statement. The
        ``update`` dictionary specifies the columns to modify and the new values.
        The keys are :class:`Column` objects representing the target columns.
        The values can be:
        - Literal Python values (e.g., integers, strings), which are
            parameterized and safely escaped.
        - Other :class:`Column` objects, to set a column to the value of
            another column (e.g., ``{table.col1: table.col2}``).
        - :class:`ColumnsOperation` expressions, to perform arithmetic,
            string concatenation, function calls, etc., computed on the
            database side.

        The ``where`` parameter is a :class:`ColumnsOperation` expression that
        defines which rows to update (e.g., ``table.id == 5``). Rows that do
        not satisfy the condition remain unchanged.

        The update is executed in a single transaction (via the driver's
        connection pool) and immediately committed unless an error occurs,
        in which case the transaction is rolled back.

        Args:
            update (dict[Column, Any]): A mapping from :class:`Column` objects
                to new values. The values can be literals, :class:`Column`
                references, or :class:`ColumnsOperation` expressions.
            where (ColumnsOperation): A condition that selects the rows to
                update. Must be a :class:`ColumnsOperation` instance, typically
                built using comparison operators (``==``, ``>``, etc.) or
                logical operators (``&``, ``|``).

        Returns:
            None: This method updates rows in the database and does not
            return any value.

        Raises:
            Exception: If the underlying SQL execution fails (e.g., syntax
                error, constraint violation, connection issue). The original
                error, query, and parameters are included in the exception
                message.

        Example (Simple)::
            # Update age of a specific user
            table = db.users
            table.update(
                update={table.age: 30},
                where=table.id == 5
            )

        Example (Complex using ColumnsOperation)::
            # Increase price by 10% for products with low stock, and set
            # description to uppercase concatenation of name and category.
            table = db.products
            table.update(
                update={
                    table.price: table.price * 1.10,
                    table.description: table.name.add_end(' - ').add_end(table.category).upper()
                },
                where=table.stock < 5
            )
        """
        if not update:
            return
        temp_list = []
        [None if isinstance(value , Column) else temp_list.append(value) if not isinstance(value, ColumnsOperation) else temp_list.extend(value._output[1]) for key, value in update.items()]
        self._excp(f"UPDATE {self.name_} SET {', '.join(f'{key.first_name} = {value.first_name}' if isinstance(value, Column) else f'{key.first_name}=%s' if not isinstance(value, ColumnsOperation) else f'{key.first_name}={value._output[0]}' for key, value in list(update.items()))} WHERE {where._output[0]};", temp_list + where._output[1])

    def get_row(self, which_columns: list['Column' | 'ColumnsOperation'], where: 'ColumnsOperation' = None, order_by: 'Column | ColumnsOperation' = None, limit: int = None, offset: int = None):
        """Retrieve rows from the table with flexible column selection,
        filtering, ordering and pagination.

        This method executes a ``SELECT`` query on the table. It supports
        specifying columns as either :class:`Column` objects or
        :class:`ColumnsOperation` expressions (which allow arithmetic,
        string functions, aliases, etc.). The results are returned as a
        list of tuples (or a list of single values if only one column is
        selected). The method automatically handles parameter binding for
        security.

        Args:
            which_columns (list[Column | ColumnsOperation]): A list of
                columns or column operations to select. Each element can be
                a :class:`Column` object or a :class:`ColumnsOperation`
                expression (e.g. ``table.price * 1.1``).
            where (ColumnsOperation, optional): A filter condition. Defaults
                to ``None`` (all rows are returned).
            order_by (Column | ColumnsOperation, optional): A :class:`Column`
                or a computed :class:`ColumnsOperation` to order the results
                by (e.g. ``tbl.val * -1`` for descending, ``tbl.name.upper()``
                for case-insensitive sorting). Defaults to ``None``.
            limit (int, optional): Maximum number of rows to return. A value
                of ``0`` returns an empty list. Defaults to ``None``.
            offset (int, optional): Number of rows to skip. MySQL requires
                ``LIMIT`` before ``OFFSET``; when ``offset`` is given
                without ``limit``, the method emits
                ``LIMIT 18446744073709551615`` (the largest possible 64-bit
                unsigned integer, which MySQL treats as "no limit").
                Defaults to ``None``.

        Returns:
            list: If ``len(which_columns) == 1``, a flat list of values
            from that column (e.g. ``[1, 2, 3]``). Otherwise, a list of
            tuples, each tuple containing the selected columns in the given
            order (e.g. ``[(1, 'Alice'), (2, 'Bob')]``).

        Raises:
            Exception: If the underlying database operation fails (e.g.
                syntax error, column does not exist). The original error and
                query are included in the exception message.

        Example:
            Simple selection with condition and ordering::

                >>> users = db.users
                >>> result = users.get_row(
                ...     which_columns=[users.name, users.age],
                ...     where=users.age > 18,
                ...     order_by=users.name
                ... )
                >>> # result: [('Alice', 25), ('Bob', 30), ...]

            Complex query with computed columns, computed ORDER BY and
            pagination::

                >>> products = db.products
                >>> result = products.get_row(
                ...     which_columns=[
                ...         products.name,
                ...         products.price * 1.10,
                ...         products.name.add_end(' - ').add_end(products.category).upper()
                ...     ],
                ...     where=(products.stock > 0) & (products.price < 100),
                ...     order_by=products.price * -1,
                ...     limit=10,
                ...     offset=20
                ... )

            Case-insensitive ordering using a string operation::

                >>> names = products.get_row(
                ...     which_columns=[products.name],
                ...     order_by=products.name.upper()
                ... )
        """
        if not which_columns:
            return
        wc, tl = [], []
        [wc.append(i.first_name) if isinstance(i, Column) else [wc.append(i._output[0]) , tl.extend(i._output[1])] for i in which_columns]
        where_params = list(where._output[1]) if where else []
        limit_clause, limit_params , ob = '', [], []
        if limit is not None:
            limit_clause = 'LIMIT %s'
            limit_params.append(limit)
        if offset is not None:
            if limit is None:
                limit_clause = 'LIMIT 18446744073709551615'
            limit_clause += ' OFFSET %s'
            limit_params.append(offset)
        ob.extend(order_by._output[1]) if isinstance(order_by, ColumnsOperation) else None
        return [row[0] for row in (self._excfp((f'SELECT {", ".join(wc)} FROM {self.name_} 'f'{f'WHERE {where._output[0]}' if where else ''} {f'ORDER BY {order_by.first_name}' if isinstance(order_by, Column) else f'ORDER BY {order_by._output[0]}' if isinstance(order_by, ColumnsOperation) else ''} {limit_clause}').strip() + ';', tl+where_params+ob+limit_params) if tl+where_params+ob+limit_params else self._excf((f'SELECT {", ".join(wc)} FROM {self.name_} 'f'{f'WHERE {where._output[0]}' if where else ''} {f'ORDER BY {order_by.first_name}' if isinstance(order_by, Column) else f'ORDER BY {order_by._output[0]}' if isinstance(order_by, ColumnsOperation) else ''} {limit_clause}').strip() + ';'))] if len(which_columns) == 1 else self._excfp((f'SELECT {", ".join(wc)} FROM {self.name_} 'f'{f'WHERE {where._output[0]}' if where else ''} {f'ORDER BY {order_by.first_name}'if isinstance(order_by, Column) else f'ORDER BY {order_by._output[0]}' if isinstance(order_by, ColumnsOperation) else ''} {limit_clause}').strip()+';', tl+where_params+ob+limit_params)if tl+where_params+ob+limit_params else self._excf((f'SELECT {", ".join(wc)} FROM {self.name_} 'f'{f'WHERE {where._output[0]}'if where else''} {f'ORDER BY {order_by.first_name}'if isinstance(order_by, Column)else f'ORDER BY {order_by._output[0]}'if isinstance(order_by, ColumnsOperation) else''} {limit_clause}').strip()+';')
        # This single line is 1381 chars — also my birth year in the Persian calendar :D
        
    def insert(self, insert: dict['Column', Any]) -> None:
        """
        Insert a single row into the table.

        This method constructs an ``INSERT INTO`` SQL statement using the provided
        column-value mapping, executes it with parameterized placeholders, and
        commits the transaction. The keys of the dictionary must be :class:`Column`
        objects belonging to this table, and the values are the data to be inserted.

        Args:
            insert (dict[Column, Any]): A dictionary mapping :class:`Column` objects
                to their corresponding values for the new row. The values can be of
                any type supported by the database driver (e.g., strings, integers,
                floats, dates, None for NULL).

        Returns:
            None

        Raises:
            Exception: If the insert fails (e.g., constraint violation, type mismatch,
                connection error). The exception message includes the original error,
                the generated query, and the parameters for debugging.

        Example:
            Assuming a :class:`Driver` instance ``db`` with a table ``users`` that
            has columns ``id`` (auto-increment), ``name``, and ``age``::

                # Insert a new user
                db.users.insert({
                    db.users.name: 'Alice',
                    db.users.age: 30
                })

            The method automatically handles parameterized queries, so values are
            safely escaped.
        """
        if not insert:
            self._exc(f'INSERT INTO {self.name_} () VALUES ();')
            return
        self._excp(f'INSERT INTO {self.name_} ({', '.join(i.first_name for i in list(insert.keys()))}) VALUES ({', '.join(f'%s' for k in insert)})', [v for v in list(insert.values())])

    def custom_execute(self, query: str, params: list = None) -> None:
        """
        Execute a custom SQL query with optional parameters.

        This method provides a flexible way to run any SQL statement on the table's
        database connection. It automatically selects the appropriate execution path
        based on whether parameters are provided. If ``params`` is given, the query
        is executed with parameter substitution using the driver's ``_excp`` method;
        otherwise, the query is executed without parameters using ``_exc``.

        Args:
            query (str): The SQL query string to execute. May contain ``%s``
                placeholders if ``params`` are provided.
            params (list, optional): A list of parameter values to substitute into
                the query. Defaults to ``None``.

        Returns:
            None

        Raises:
            Exception: If the query execution fails. The original error and query
                are included in the exception message (propagated from the driver).

        Example:
            Executing a custom update with parameters::

                table = db.users
                table.custom_execute(
                    "UPDATE users SET active = %s WHERE id = %s",
                    [1, 42]
                )

            Executing a query without parameters::

                table.custom_execute("TRUNCATE TABLE logs")
        """
        self._excp(query, params) if params else self._exc(query)
            
    def custom_execute_many(self, query: str, params: list = None) -> None:
        """
        Execute a parameterized query multiple times with different parameter sets.

        This method is a convenience wrapper around :meth:`_excm` that allows
        executing the same SQL query repeatedly with a list of parameter sets.
        It is useful for performing batch inserts or updates where many rows
        need to be inserted or updated in a single round-trip to the database.
        The query should contain placeholders (``%s``) for parameters, and the
        `params` argument should be a list of tuples or lists, each representing
        one set of parameters.

        Args:
            query (str): The SQL query string with ``%s`` placeholders for
                parameters.
            params (list, optional): A list of parameter sequences (tuples or lists)
                to be substituted into the query. Each inner sequence corresponds
                to one execution. If ``None``, the method raises an error or
                behaves unpredictably; this parameter is required for this method.

        Returns:
            None

        Raises:
            Exception: If the query execution fails (e.g., syntax error,
                constraint violation, or connection issue). The original error
                and the query/parameters are included in the exception message.

        Example:
            Batch inserting multiple rows into a table::

                table = db.users
                query = "INSERT INTO users (name, age) VALUES (%s, %s)"
                data = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
                table.custom_execute_many(query, data)
        """
        self._excm(query, params)

    def custom_execute_with_fetch(self, query: str, params: list = None) -> Any:
        """
        Execute a custom SQL query and return the fetched results.

        This method allows executing arbitrary parameterized or non-parameterized
        SQL queries and retrieves all result rows. It is useful for complex
        queries that are not covered by the built-in ORM methods. If ``params``
        are provided, the query is executed with parameter substitution; otherwise,
        it is executed as a plain query. The result is the full set of rows
        returned by the query.

        Args:
            query (str): The SQL query string. May contain ``%s`` placeholders
                for parameters if ``params`` is provided.
            params (list, optional): A list or tuple of parameter values to
                substitute into the query. Defaults to ``None``, meaning the
                query has no parameters.

        Returns:
            Any: The fetched result, typically a list of tuples where each tuple
            represents a row. The exact structure depends on the query.

        Raises:
            Exception: If the query execution fails (e.g., syntax error,
                connection issue, or invalid parameters). The original error
                and query details are included in the exception message.

        Example:
            Executing a custom SELECT query with parameters::

                table = db.users
                results = table.custom_execute_with_fetch(
                    "SELECT id, name FROM users WHERE age > %s",
                    [25]
                )
                for row in results:
                    print(f"ID: {row[0]}, Name: {row[1]}")

            Executing a query without parameters::

                results = table.custom_execute_with_fetch(
                    "SELECT COUNT(*) FROM users"
                )
                count = results[0][0] if results else 0
        """
        return self._excfp(query, params) if params else self._excf(query)

    def delete_row(self, where: 'ColumnsOperation') -> None:
        """
        Delete rows from the table that match a given condition.

        This method executes a ``DELETE FROM`` SQL statement with a ``WHERE``
        clause constructed from the provided :class:`ColumnsOperation` object.
        The operation is performed immediately and the transaction is committed
        (or rolled back on failure) by the underlying connection.

        Args:
            where (ColumnsOperation): A condition object that defines which rows
                to delete. The condition is typically built using column objects
                and comparison operators (e.g., ``column == value``,
                ``column > other_column``, etc.). The object must have a valid
                ``_output`` attribute with the SQL condition string and the
                corresponding parameter list.

        Returns:
            None

        Raises:
            Exception: If the query execution fails (e.g., invalid condition,
                connection issue, or other database error). The original error,
                query, and parameters are included in the exception message.

        Example:
            Assuming a ``users`` table and a configured :class:`Driver` instance
            ``db``::

                # Delete a user with a specific ID
                db.users.delete_row(db.users.id == 5)

                # Delete users older than 30
                db.users.delete_row(db.users.age > 30)

                # Delete users whose name starts with 'A'
                db.users.delete_row(db.users.name.startswith('A'))
        """
        self._excp(f'DELETE FROM {self.name_} WHERE {where._output[0]};', where._output[1])

    def delete_table(self, are_you_sure: bool, are_you_really_sure: bool, for_sure: bool) -> None:
        """
        Permanently delete the entire table from the database.

        This method executes a ``DROP TABLE`` statement, which irreversibly removes
        the table and all its data, indexes, and constraints. To prevent accidental
        deletion, three explicit confirmation flags are required. All three must be
        ``True`` for the operation to proceed. After successful deletion, the table
        attribute is also removed from the parent :class:`Driver` instance.

        Args:
            are_you_sure (bool): First-level confirmation flag.
            are_you_really_sure (bool): Second-level confirmation flag.
            for_sure (bool): Final confirmation flag.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., insufficient
                privileges or the table does not exist). The original error and
                query are included in the exception message.

        Example:
            Assuming a :class:`Table` instance named ``users`` attached to a
            :class:`Driver` instance ``db``::

                # Danger: this deletes the 'users' table
                users.delete_table(True, True, True)

            If any flag is ``False``, nothing happens::

                users.delete_table(True, True, False)  # No effect
        """
        if are_you_sure and are_you_really_sure and for_sure:
            self._exc(f'DROP TABLE {self.name_};')
            self.db_obj.__delattr__(self.name_[1:-1])

    def delete_column(
        self,
        column: 'Column',
        are_you_sure: bool,
        are_you_really_sure: bool,
        for_sure: bool
    ) -> None:
        """
        Permanently delete a column from the table.

        This method executes an ``ALTER TABLE ... DROP COLUMN`` statement to remove
        the specified column from the table schema. All data stored in that column
        is irreversibly lost. To prevent accidental deletion, three explicit
        confirmation flags are required; all three must be ``True`` for the
        operation to proceed. After successful deletion, the corresponding
        :class:`Column` attribute is also removed from the :class:`Table` instance.

        Args:
            column (Column): The :class:`Column` object representing the column
                to delete.
            are_you_sure (bool): First-level confirmation flag.
            are_you_really_sure (bool): Second-level confirmation flag.
            for_sure (bool): Final confirmation flag.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., the column does
                not exist, insufficient privileges, or the table is locked). The
                original error and query are included in the exception message.

        Example:
            Assuming a :class:`Table` instance named ``users`` that has a column
            ``temp_data``::

                # Danger: this deletes the 'temp_data' column
                users.delete_column(users.temp_data, True, True, True)

            If any flag is ``False``, nothing happens::

                users.delete_column(users.temp_data, True, True, False)  # No effect
        """
        if are_you_sure and are_you_really_sure and for_sure:
            self._exc(f'ALTER TABLE {self.name_} DROP COLUMN {column.first_name};')
            self.__delattr__(column.first_name[1:-1])

    def add_column(
        self,
        column_name: str,
        data_type: str,          # خروجی از DataTypes (مثلاً DataTypes.INT())
        nullable: bool = True,
        default: Any = None,
        auto_increment: bool = False,
        primary_key: bool = False,
        unique: bool = False,
        comment: str = None,
        after: Column = None,
        first: bool = False
    ) -> None:
        """
        Add a new column to the table.

        This method executes an ``ALTER TABLE ADD COLUMN`` statement to add a new
        column to the existing table. It supports various column options such as
        nullability, default value, auto‑increment, uniqueness, primary key, and
        positioning (via the ``after`` or ``first`` parameters). If ``primary_key``
        is ``True``, an additional ``ALTER TABLE ADD PRIMARY KEY`` statement is
        executed. After the column is added, the method dynamically attaches a
        :class:`Column` object to the current :class:`Table` instance, making the
        new column accessible as an attribute (e.g., ``table.new_column``).

        Args:
            column_name (str): The name of the new column. It will be quoted
                automatically.
            data_type (str): The SQL data type definition, typically returned by
                one of the static methods in :class:`DataTypes` (e.g.,
                ``DataTypes.INT()``, ``DataTypes.VARCHAR(255)``).
            nullable (bool, optional): If ``True``, the column allows ``NULL``
                values. If ``False``, the column is ``NOT NULL``. Defaults to
                ``True``.
            default (Any, optional): The default value for the column. If provided,
                it will be used in the ``DEFAULT`` clause. For strings, the value
                is automatically quoted. Defaults to ``None`` (no default).
            auto_increment (bool, optional): If ``True``, the column is set to
                ``AUTO_INCREMENT``. This is typically used for numeric primary keys.
                Defaults to ``False``.
            primary_key (bool, optional): If ``True``, the column is added as the
                primary key (or part of it) via a separate ``ALTER TABLE ADD
                PRIMARY KEY`` statement. Defaults to ``False``.
            unique (bool, optional): If ``True``, the column is defined as
                ``UNIQUE``. Defaults to ``False``.
            comment (str, optional): A comment for the column, added as
                ``COMMENT '...'``. Defaults to ``None``.
            after (Column, optional): An existing :class:`Column` object after
                which the new column should be placed. This is translated to the
                ``AFTER column_name`` clause. Defaults to ``None``.
            first (bool, optional): If ``True``, the new column is placed at the
                beginning of the table (``FIRST`` clause). Defaults to ``False``.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., invalid data
                type, duplicate column name, constraint violation, or permission
                error). The original error and query are included in the exception
                message.

        Example:
            Adding a new ``email`` column to an existing ``users`` table::

                from ormophine.Mysql import DataTypes

                # Assuming `users` is a Table instance
                users.add_column(
                    column_name='email',
                    data_type=DataTypes.VARCHAR(255),
                    nullable=False,
                    unique=True,
                    comment='User email address',
                    after=users.id
                )

            Adding an auto‑increment primary key column::

                users.add_column(
                    column_name='id',
                    data_type=DataTypes.INT(),
                    nullable=False,
                    auto_increment=True,
                    primary_key=True,
                    first=True
                )
        """
        self._exc(f'ALTER TABLE {self.name_} ADD COLUMN {column_name} {data_type} {' NOT NULL' if not nullable else ''}{' AUTO_INCREMENT' if auto_increment else ''}{f" DEFAULT '{default}'" if default is not None and isinstance(default,str) else f' DEFAULT {default}' if default is not None else ''}{' UNIQUE' if unique else ''}{f" COMMENT '{comment}'" if comment else ''}{' FIRST' if first else f" AFTER {after.first_name[1:-1]}" if after else ''};')
        self._exc(f"ALTER TABLE {self.name_} ADD PRIMARY KEY (`{column_name}`);") if primary_key else ''
        self.__setattr__(column_name, Column(self, column_name, int if data_type.lower().split('(')[0] in ('int', 'integer', 'tinyint', 'smallint', 'mediumint', 'bigint', 'serial', 'year', 'bit') else float if data_type.lower().split('(')[0] in ('real', 'float', 'double', 'decimal', 'numeric') else   bool if data_type.lower().split('(')[0] == 'boolean' else object if data_type.lower().split('(')[0] in ('date', 'time', 'datetime', 'timestamp')else str if data_type.lower().split('(')[0] in ('char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext', 'enum', 'set', 'json')else bytes if data_type.lower().split('(')[0] in ('blob', 'tinyblob', 'mediumblob', 'longblob', 'binary', 'varbinary', 'geometry', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection') else str))

    def rename_table(self, new_name: str) -> None:
        """
        Rename the database table and update the corresponding attribute on the driver.

        This method executes an ``ALTER TABLE ... RENAME TO`` statement to change the
        table's name in the database. After a successful rename, it removes the old
        table attribute from the parent :class:`Driver` instance and adds a new
        attribute with the new name, which is a fresh :class:`Table` instance
        representing the renamed table. The internal ``name_`` attribute is also
        updated.

        Args:
            new_name (str): The new name for the table. Must be a valid MySQL
                table name identifier.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., the new name
                already exists, insufficient privileges, or a syntax error). The
                original error and query are included in the exception message.

        Example:
            Assuming a :class:`Table` instance named ``old_users`` attached to a
            :class:`Driver` instance ``db``::

                # Rename the table from 'old_users' to 'users'
                old_users.rename_table('users')

                # After renaming, the table is accessible as db.users
                db.users.insert({'name': 'Alice'})
        """
        self._exc(f'ALTER TABLE {self.name_} RENAME TO `{new_name}`;')
        self.db_obj.__delattr__(self.name_[1:-1])
        self.db_obj.__setattr__(new_name, Table(obj=self.db_obj, table_name=new_name))
        self.name_ = f'`{new_name}`'


    def rename_column(self, column: 'Column', new_name: str) -> None:
        """
        Rename an existing column in the table.

        This method alters the table schema by changing the name of a column
        while preserving its data type, constraints, and other attributes.
        It retrieves the current full column type definition using
        :meth:`get_table_info`, constructs an ``ALTER TABLE ... CHANGE COLUMN``
        statement, and executes it. After a successful rename, the table's
        dynamic attribute for the column is updated to reflect the new name,
        and the old attribute is removed.

        Note:
            This operation may fail if the new name already exists or if the
            column is involved in foreign key constraints that do not allow
            renaming. The specific behavior depends on the database engine.

        Args:
            column (Column): The :class:`Column` object representing the column
                to be renamed. This object must belong to this table.
            new_name (str): The new name for the column. Must be a valid
                identifier (not a reserved keyword) and unique within the table.

        Returns:
            None

        Raises:
            Exception: If the underlying SQL execution fails (e.g., column does not
                exist, new name already in use, permission denied, or foreign key
                constraint). The original error message and query are included in
                the exception.

        Example:
            Renaming a column in a ``users`` table::

                from ormophine.Mysql import Table, Driver

                # Assume db is a Driver instance and users is a Table
                users = db.users
                old_col = users.username
                users.rename_column(old_col, 'user_name')

                # The column is now accessible as users.user_name
                users.insert({'user_name': 'alice', 'email': 'alice@example.com'})
        """
        for col_info in self.get_table_info():
            if col_info['name'] == column.first_name[1:-1]:
                full_type = col_info['full_type']  
                break
        query = f'ALTER TABLE {self.name_} CHANGE COLUMN {column.first_name} `{new_name}` {full_type};'
        self._exc(query)
        self.__delattr__(column.first_name[1:-1])
        self.__setattr__(new_name, Column(self, new_name, column.datatype))

    def create_index(
        self,
        index_name: str,
        columns: list['Column'],
        unique: bool = False,
        where: 'ColumnsOperation' = None
    ) -> None:
        """
        Create an index on one or more columns of the table.

        This method generates and executes a ``CREATE INDEX`` statement. If the
        ``unique`` parameter is ``True``, a unique index is created, enforcing
        uniqueness of the indexed column values. If a ``where`` condition is
        provided, the index is created as a filtered (partial) index, which only
        includes rows that satisfy the condition. The condition is constructed
        using a :class:`ColumnsOperation` object, and its parameter values are
        substituted directly into the SQL string (since MySQL does not support
        parameter placeholders in index definitions).

        Args:
            index_name (str): The name of the index to create.
            columns (list[Column]): A list of :class:`Column` objects specifying
                the columns to include in the index.
            unique (bool, optional): If ``True``, a unique index is created.
                Defaults to ``False``.
            where (ColumnsOperation, optional): A condition expression that defines
                which rows to include in the index. If provided, only rows matching
                this condition are indexed. Defaults to ``None``.

        Returns:
            None

        Raises:
            Exception: If the SQL execution fails (e.g., index name already exists,
                invalid column names, or permission denied). The original error
                and the generated query are included in the exception.

        Example:
            Creating a non‑unique index on a single column::

                users.create_index('idx_users_name', [users.name])

            Creating a unique index on multiple columns::

                users.create_index('idx_users_email_unique', [users.email], unique=True)

            Creating a filtered index on active users::

                from ormophine.Mysql import ColumnsOperation

                # Assuming users.active is a Column
                condition = users.active == 1
                users.create_index('idx_users_active', [users.last_login], where=condition)
        """
        if where:
            wr = f'WHERE {where._output[0]}'
            for i in where._output[1]:
                wr=wr.replace('%s',i if isinstance(i,str) else str(i),1)
        self._excp(f'CREATE {'UNIQUE ' if unique else ''}INDEX {index_name} ON {self.name_} ({','.join(i.first_name for i in columns)}) {wr if where else ''}',[])

    def delete_index(self, index_name: str) -> None:
        """
        Drop an index from the table.

        This method executes a ``DROP INDEX`` statement to permanently remove an
        existing index from the table. The index name must exactly match the name
        used when the index was created (case‑sensitive depending on the database
        and collation settings). Dropping an index can improve write performance
        but may negatively affect read queries that relied on the index.

        Args:
            index_name (str): The name of the index to drop.

        Returns:
            None

        Raises:
            Exception: If the index does not exist, if the user lacks sufficient
                privileges, or if a database error occurs. The original error
                and the executed query are included in the exception message.

        Example:
            Dropping an index named ``idx_users_email`` from the ``users`` table::

                users.delete_index('idx_users_email')
        """
        self._exc(f'DROP INDEX {index_name} ON {self.name_};')

    def get_indexes_info(self) -> Any:
        """
        Retrieve detailed information about all indexes on the table.

        This method executes a ``SHOW INDEX FROM`` query and returns a list of
        dictionaries, each containing comprehensive metadata about an index,
        including its name, uniqueness, column(s), collation, cardinality, and
        other properties. The result is similar to the output of MySQL's
        ``SHOW INDEX`` statement but formatted as a list of dicts for easier
        programmatic access.

        Returns:
            list of dict: A list where each element is a dictionary with the
            following keys:

            - ``idx_name`` (str): The name of the index.
            - ``non_unique`` (bool): ``True`` if the index allows duplicate values,
            ``False`` if it is a unique index.
            - ``seq_in_idx`` (int): The column sequence number within the index
            (starting from 1).
            - ``Columns_name`` (str): The name of the column that is part of the
            index.
            - ``collation`` (str): The collation order (e.g., ``'A'`` for
            ascending, ``'D'`` for descending, or ``None`` if not applicable).
            - ``cardinality`` (int): An estimate of the number of unique values in
            the index.
            - ``sub_part`` (int): The index prefix length (if the column is only
            partially indexed), or ``None``.
            - ``packed`` (str): Indicates whether the index is packed (usually
            ``None``).
            - ``nullable`` (str): ``'YES'`` if the column can contain ``NULL``
            values, otherwise ``'NO'``.
            - ``idx_type`` (str): The index type (e.g., ``'BTREE'``, ``'HASH'``).
            - ``comment`` (str): Any comment associated with the index.

        Raises:
            Exception: If the underlying query fails (e.g., the table does not
                exist or the user lacks privileges). The original error and the
                executed query are included in the exception message.

        Example:
            Retrieving index information for the ``users`` table::

                from ormophine.Mysql import Driver

                db = Driver(host='localhost', port=3306, username='root',
                            password='pass', db_name='myapp')
                users = db.users
                indexes = users.get_indexes_info()
                for idx in indexes:
                    print(f"Index: {idx['idx_name']}, Column: {idx['Columns_name']}, "
                        f"Unique: {idx['non_unique']}")
                # Output example:
                # Index: PRIMARY, Column: id, Unique: False
                # Index: idx_users_email, Column: email, Unique: True
        """
        return [{'idx_name':i[2], 'non_unique':bool(i[1]),'unique': not bool(i[1]), 'seq_in_idx':i[3], 'Columns_name':i[4],'collation':i[5], 'cardinality':i[6], 'sub_part':i[7], 'packed':i[8], 'nullable':i[9], 'idx_type':i[10], 'comment':i[11]} for i in self._excf(f'SHOW INDEX FROM {self.name_}')]

    def bulk_insert(self, columns: list['Column'], data_list: list) -> None:
        """
        Insert multiple rows into the table in a single efficient operation.

        This method uses the ``executemany`` feature of the underlying DB driver
        to insert many rows at once. It takes a list of :class:`Column` objects
        specifying the target columns and a list of rows (each row being a list
        or tuple of values) to insert. The number and order of values in each
        row must match the specified columns.

        Bulk insertion is significantly faster than inserting rows individually
        when dealing with large datasets because it reduces the number of round‑trips
        to the database.

        Args:
            columns (list['Column']): A list of :class:`Column` objects representing
                the columns into which data will be inserted. The order of columns
                determines the order of values expected in each row of ``data_list``.
            data_list (list): A list of rows, where each row is a list or tuple of
                values corresponding to the specified columns. All rows must have
                the same length and the values must be compatible with the column
                data types.

        Returns:
            None

        Raises:
            Exception: If a database error occurs (e.g., data type mismatch, duplicate
                key violation, or connection issues). The original error and the
                executed query are included in the exception message.

        Example:
            Inserting multiple users into a ``users`` table::

                # Assume `users` is a Table instance with columns: id, name, age
                users.bulk_insert(
                    columns=[users.name, users.age],
                    data_list=[
                        ['Alice', 30],
                        ['Bob', 25],
                        ['Charlie', 35]
                    ]
                )
                # This will execute a single INSERT with multiple rows.
        """
        self._excm(f'INSERT INTO {self.name_} ({', '.join(i.first_name for i in columns)}) VALUES ({', '.join('%s' for i in columns)});',data_list)

    def bulk_update(self, update: dict['Column', Any], where: 'ColumnsOperation', data_list: list) -> None:
        """Execute a bulk UPDATE operation with parameterized placeholders.

        This method performs a single UPDATE statement for multiple rows by
        using placeholders (``PLACE_HOLDER``) that are replaced with values
        from each row in ``data_list``. It is designed for efficient batch
        updates where the same update structure applies to many rows, but the
        specific values differ per row.

        The ``update`` dictionary and the ``where`` condition can contain the
        special placeholder ``self.PLACE_HOLDER`` (or ``db.PLACE_HOLDER``) to
        indicate that the actual value should be taken from the corresponding
        position in each row of ``data_list``. The method constructs the final
        SQL by replacing ``%s`` placeholders with ``PLACE_HOLDER``, builds a
        parameterized query, and then executes it using ``executemany`` with
        the ``data_list``.

        The number of ``PLACE_HOLDER`` occurrences across ``update`` values
        and the ``where`` clause must equal the number of items in each row
        of ``data_list``. The order of ``PLACE_HOLDER`` occurrences in the
        final SQL is preserved (values from ``update`` first, in dict order,
        then values from ``where``).

        Args:
            update (dict[Column, Any]): A dictionary mapping :class:`Column`
                objects to new values. Values can be literals, :class:`Column`
                objects (for column-to-column assignment), or
                :class:`ColumnsOperation` objects (which may embed
                ``PLACE_HOLDER``). Use ``PLACE_HOLDER`` for values that should
                come from ``data_list``.
            where (ColumnsOperation): A :class:`ColumnsOperation` representing
                the condition that determines which rows to update. May also
                contain ``PLACE_HOLDER`` to be substituted from ``data_list``.
            data_list (list): A list of rows, where each row is a list/tuple
                of values corresponding to the ``PLACE_HOLDER`` occurrences in
                ``update`` and ``where`` (in order of appearance).

        Returns:
            None: This method executes the bulk update and does not return a
            value.

        Raises:
            Exception: If the number of ``PLACE_HOLDER`` occurrences does not
                match the number of items in each row of ``data_list``. The
                error message suggests either checking the placeholder count
                or changing ``table.PLACE_HOLDER`` to a custom string that
                does not collide with your data. Also propagates other
                database errors.

        Example:
            Simple bulk update using placeholders for column values::

                >>> employees = db.employees
                >>> employees.bulk_update(
                ...     {employees.salary: employees.salary + employees.PLACE_HOLDER},
                ...     employees.department == employees.PLACE_HOLDER,
                ...     data_list=[
                ...         [5000, "Engineering"],
                ...         [3000, "Marketing"],
                ...         [4000, "Sales"],
                ...     ]
                ... )
                >>> # Generates: UPDATE `employees` SET `salary` = (`salary` + %s)
                >>> #             WHERE `department` = %s;
                >>> # Executes with the given data_list.

            Complex bulk update with multiple placeholders and a compound
            condition::

                >>> employees.bulk_update(
                ...     {
                ...         employees.bonus: employees.salary * employees.PLACE_HOLDER / 100,
                ...         employees.title: employees.title + " (Senior)"
                ...     },
                ...     (employees.title == "Manager") &
                ...     (employees.years > employees.PLACE_HOLDER),
                ...     data_list=[[10, 5], [15, 8], [12, 6]]
                ... )
                >>> # First PLACE_HOLDER (percentage) comes from update,
                >>> # second PLACE_HOLDER (years threshold) comes from where.
                >>> # Each row provides [percentage, years_threshold].

        Note:
            The placeholder string is reserved by the ORM. If your data
            legitimately contains the same literal string, you can change
            the placeholder per table::

                employees.PLACE_HOLDER = "my_own_marker_%s"

            MySQL's ``executemany`` uses the same parameter style (``%s``)
            as :meth:`custom_execute_many`, so the underlying driver call is
            identical to a manual ``executemany`` loop, but the SET/WHERE
            values are pulled from ``data_list`` in a single round-trip.
        """
        temp_list = []
        [None if isinstance(value , Column) else temp_list.append(value) if not isinstance(value, ColumnsOperation) else temp_list.extend(value._output[1]) for key, value in update.items()]
        query_splited = f'UPDATE {self.name_} SET {', '.join(f'{key.first_name} = {value.first_name}' if isinstance(value, Column) else f'{key.first_name}={str(self.PLACE_HOLDER)}' if not isinstance(value , ColumnsOperation) else f'{key.first_name}={value._output[0].replace('%s', str(self.PLACE_HOLDER))}' for key , value in list(update.items()))} WHERE {where._output[0].replace('%s', str(self.PLACE_HOLDER))};'.split(str(self.PLACE_HOLDER))
        query= query_splited[0]
        for a,i in enumerate(temp_list+where._output[1]):
            query = query +( f'"{i}"' if isinstance(i,str) and not i == str(self.PLACE_HOLDER) else str(i))+ query_splited[a+1] #All "? || '%'" thing are because of Column.contain() method and .startswith() and .endswith() that have "%" in output value
        try:
            self._excm(query.replace(str(self.PLACE_HOLDER), '%s'), data_list)
        except Exception as e:
            if "Incorrect number of bindings" in str(e):
                raise Exception(f'number of `PLACE_HOLDERS` must be equals to number of items in each of `data_list` items.\n if it is so, make sure that there is no "{str(self.PLACE_HOLDER)}" literal string in your query because it is reserved for this orm. you can change it on you own need with `mytable.PLACE_HOLDER = "you own idea"`')
            else:
                raise

    def inner_join(self, table: 'Table', condition: 'ColumnsOperation') -> 'JoinQuery':
        """
        Start a JOIN query with an INNER JOIN.

        Returns a :class:`JoinQuery` that supports chaining more joins
        (via ``inner_join``, ``left_join``, ``right_join``) and finally
        executing with ``get_row(...)``.

        Args:
            table (Table): The table to join (right side).
            condition (ColumnsOperation): The ON condition (e.g.
                ``orders.user_id == users.id``).

        Returns:
            JoinQuery: A chainable query builder.
        """
        return JoinQuery(self).inner_join(table, condition)

    def left_join(self, table: 'Table', condition: 'ColumnsOperation') -> 'JoinQuery':
        """
        Start a JOIN query with a LEFT JOIN.
        """
        return JoinQuery(self).left_join(table, condition)

    def right_join(self, table: 'Table', condition: 'ColumnsOperation') -> 'JoinQuery':
        """
        Start a JOIN query with a RIGHT JOIN.

        Note: Both MySQL and MariaDB support RIGHT JOIN natively.
        """
        return JoinQuery(self).right_join(table, condition)

from __future__ import annotations
from typing import Literal

class DataTypes:
    """
    Complete MySQL 8.0 Data Types as static methods.

    This class provides a comprehensive set of static methods that return SQL
    data type strings for use in table definitions. Each method corresponds to
    a MySQL 8.0 data type, including numeric, string, date/time, spatial, JSON,
    and special types. The returned strings can be directly used in
    :class:`TableStructure` column definitions (e.g., via
    :meth:`TableStructure.add_column`).

    The class also defines the :attr:`TEXT_SIZE` type variable for use with
    the :meth:`TEXT` method.

    All methods are static and do not require an instance. Simply call
    ``DataTypes.INT()``, ``DataTypes.VARCHAR(255)``, etc. Each method accepts
    appropriate parameters (e.g., length, precision, unsigned flags) to tailor
    the generated SQL type definition.

    Examples:
        Defining a table with various data types:

        >>> from ormophine.Mysql import TableStructure, DataTypes
        >>> table = (TableStructure('employees')
        ...          .add_column('id', DataTypes.SERIAL(), primary_key=True,
        ...                      auto_increment=True, not_null=True)
        ...          .add_column('name', DataTypes.VARCHAR(100), not_null=True)
        ...          .add_column('salary', DataTypes.DECIMAL(10,2))
        ...          .add_column('birth_date', DataTypes.DATE())
        ...          .add_column('metadata', DataTypes.JSON()))

        Using the `TEXT_SIZE` literal with :meth:`TEXT`:

        >>> DataTypes.TEXT('LONGTEXT')
        'LONGTEXT'
        >>> DataTypes.TEXT()
        'TEXT'

    This class is intended to be used with the ORM's table creation and
    alteration utilities. All methods are guaranteed to return valid MySQL 8.0
    syntax.
    """
    TEXT_SIZE = Literal['TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT']
    # ========================
    # Numeric Data Types
    # ========================

    @staticmethod
    def BIT(size: int) -> str:
        """
        Generate a MySQL BIT data type definition.

        The BIT type stores bit-field values, where the length specifies the number
        of bits per value. Valid sizes range from 1 to 64 bits. This method returns
        a string suitable for use in ``CREATE TABLE`` or ``ALTER TABLE`` statements.

        Args:
            size (int): The number of bits in the field. Must be between 1 and 64
                inclusive.

        Returns:
            str: A SQL data type string in the format ``BIT(size)``.

        Raises:
            ValueError: If ``size`` is outside the valid range (1–64).

        Example:
            Create a table with a BIT column::

                from ormophine.Mysql import DataTypes, TableStructure

                struct = (TableStructure('settings')
                        .add_column('id', DataTypes.INT(), primary_key=True,
                                    auto_increment=True, not_null=True)
                        .add_column('flags', DataTypes.BIT(8), default_value=0))

                # The 'flags' column will be defined as BIT(8) in SQL.
        """
        if size:
            if size < 1 or size > 64:
                raise ValueError("Size for BIT must be between 1 and 64.")
        return f"BIT({size})"

    @staticmethod
    def TINYINT(size: int = None, unsigned: bool = False, zerofill: bool = False) -> str:
        """
        Generate a MySQL TINYINT data type definition.

        TINYINT is a very small integer. When signed, its range is -128 to 127.
        When unsigned, the range is 0 to 255. This method returns the SQL string
        that can be used in a ``CREATE TABLE`` or ``ALTER TABLE`` statement.

        Args:
            size (int, optional): The display width. If provided, it must be within
                the signed or unsigned range accordingly. Defaults to ``None``,
                meaning no display width is specified.
            unsigned (bool, optional): If ``True``, the column is defined as
                ``UNSIGNED``, disallowing negative values. Defaults to ``False``.
            zerofill (bool, optional): If ``True``, the column is defined as
                ``ZEROFILL``, which implies ``UNSIGNED`` and pads values with leading
                zeros up to the display width. Defaults to ``False``.

        Returns:
            str: The MySQL TINYINT data type string, e.g., ``"TINYINT(3) UNSIGNED"``.

        Raises:
            ValueError: If the provided ``size`` is outside the valid range for
                the signed or unsigned mode.

        Example:
            Create a table with a TINYINT column::

                from ormophine.Mysql import DataTypes

                # Signed TINYINT with display width 3
                tiny_signed = DataTypes.TINYINT(size=3)

                # Unsigned TINYINT without display width
                tiny_unsigned = DataTypes.TINYINT(unsigned=True)

                # ZEROFILL TINYINT (implies UNSIGNED)
                tiny_zerofill = DataTypes.TINYINT(size=5, zerofill=True)

                # Use in a table structure
                from ormophine.Mysql import TableStructure
                table = (TableStructure('scores')
                        .add_column('score', tiny_unsigned, not_null=True))
        """
        if size is not None:
            if (size < -128 or size > 127) and not unsigned or (size < 0 or size > 255) and unsigned:
                raise ValueError("Size for TINYINT must be between -128 and 127 for signed or 0 to 255 for unsigned.")
        result = "TINYINT"
        if size:
            result += f"({size})"
        if unsigned:
            result += " UNSIGNED"
        if zerofill:
            result += " ZEROFILL"
        return result

    @staticmethod
    def SMALLINT(size: int = None, unsigned: bool = False, zerofill: bool = False) -> str:
        """
        Return the SQL data type string for a SMALLINT column.

        SMALLINT is a small integer type. The signed range is -32768 to 32767,
        and the unsigned range is 0 to 65535. The optional ``size`` parameter
        affects display width but does not change the storage size or value range.
        The ``unsigned`` and ``zerofill`` flags can be used to modify the column
        definition.

        Args:
            size (int, optional): The display width of the column. If provided,
                it must be within the valid range for the chosen signed/unsigned
                mode. Defaults to ``None``, meaning the default display width is used.
            unsigned (bool, optional): If ``True``, the column is defined as
                ``UNSIGNED``, disallowing negative values. Defaults to ``False``.
            zerofill (bool, optional): If ``True``, the column is defined as
                ``ZEROFILL``, which pads displayed values with leading zeros up to
                the display width. Implies ``UNSIGNED``. Defaults to ``False``.

        Returns:
            str: The SQL data type definition, e.g., ``'SMALLINT'``,
            ``'SMALLINT(5)'``, or ``'SMALLINT(5) UNSIGNED ZEROFILL'``.

        Raises:
            ValueError: If the provided ``size`` falls outside the allowed range
                for the chosen signed/unsigned mode. For signed, the allowed
                range is -32768 to 32767; for unsigned, 0 to 65535.

        Example:
            Defining a SMALLINT column with a display width and unsigned flag::

                from ormophine.Mysql import DataTypes

                sql_type = DataTypes.SMALLINT(size=5, unsigned=True)
                # Returns: 'SMALLINT(5) UNSIGNED'

                # Using it in a table structure
                table = (TableStructure('products')
                        .add_column('stock', DataTypes.SMALLINT(unsigned=True, not_null=True)))
        """
        if size is not None:
            if (size < -32768 or size > 32767) and not unsigned or (size < 0 or size > 65535) and unsigned:
                raise ValueError("Size for SMALLINT must be between -32768 and 32767 for signed or 0 to 65535 for unsigned.")
        result = "SMALLINT"
        if size:
            result += f"({size})"
        if unsigned:
            result += " UNSIGNED"
        if zerofill:
            result += " ZEROFILL"
        return result

    @staticmethod
    def MEDIUMINT(size: int = None, unsigned: bool = False, zerofill: bool = False) -> str:
        """
        Return the SQL data type string for a MEDIUMINT column.

        MEDIUMINT is a medium‑sized integer type. The signed range is
        -8,388,608 to 8,388,607, and the unsigned range is 0 to 16,777,215.
        The optional ``size`` parameter controls the display width but does
        not affect storage size or the range of values. The ``unsigned`` and
        ``zerofill`` flags modify the column definition as described below.

        Args:
            size (int, optional): The display width of the column. If provided,
                it must be within the valid range for the chosen signed/unsigned
                mode. Defaults to ``None``, which uses the default display width.
            unsigned (bool, optional): If ``True``, the column is defined as
                ``UNSIGNED``, disallowing negative values. Defaults to ``False``.
            zerofill (bool, optional): If ``True``, the column is defined as
                ``ZEROFILL``, which pads displayed values with leading zeros up to
                the display width. This option implicitly makes the column
                ``UNSIGNED``. Defaults to ``False``.

        Returns:
            str: The SQL data type definition, e.g., ``'MEDIUMINT'``,
            ``'MEDIUMINT(8)'``, or ``'MEDIUMINT(8) UNSIGNED ZEROFILL'``.

        Raises:
            ValueError: If the provided ``size`` falls outside the allowed range
                for the chosen signed/unsigned mode. For signed, the allowed
                range is -8,388,608 to 8,388,607; for unsigned, 0 to 16,777,215.

        Example:
            Defining a MEDIUMINT column with a display width and unsigned flag::

                from ormophine.Mysql import DataTypes

                sql_type = DataTypes.MEDIUMINT(size=8, unsigned=True)
                # Returns: 'MEDIUMINT(8) UNSIGNED'

                # Using it in a table structure
                table = (TableStructure('logs')
                        .add_column('event_count', DataTypes.MEDIUMINT(unsigned=True, not_null=True)))
        """
        if size is not None:
            if (size < -8388608 or size > 8388607) and not unsigned or (size < 0 or size > 16777215) and unsigned:
                raise ValueError("Size for MEDIUMINT must be between -8388608 and 8388607 for signed or 0 to 16777215 for unsigned.")
        result = "MEDIUMINT"
        if size:
            result += f"({size})"
        if unsigned:
            result += " UNSIGNED"
        if zerofill:
            result += " ZEROFILL"
        return result

    @staticmethod
    def INT(size: int = None, unsigned: bool = False, zerofill: bool = False) -> str:
        """
        Return the SQL data type string for an INT (standard integer) column.

        INT is a standard integer type in MySQL. The signed range is -2147483648 to
        2147483647, and the unsigned range is 0 to 4294967295. The optional ``size``
        parameter defines the display width (e.g., ``INT(11)``) but does not affect
        storage size or value range. The ``unsigned`` flag disallows negative values,
        and ``zerofill`` pads displayed values with leading zeros up to the display
        width (which also implies unsigned in standard MySQL, though the ORM allows
        setting them independently).

        Args:
            size (int, optional): The display width of the column. If provided, it
                must fall within the valid range for the chosen signed/unsigned mode.
                For signed, valid values are between -2147483648 and 2147483647;
                for unsigned, between 0 and 4294967295. Defaults to ``None``, which
                uses the default display width.
            unsigned (bool, optional): If ``True``, the column is defined as
                ``UNSIGNED``, disallowing negative values. Defaults to ``False``.
            zerofill (bool, optional): If ``True``, the column is defined as
                ``ZEROFILL``, padding displayed numeric values with leading zeros.
                In MySQL, this typically implies ``UNSIGNED``. Defaults to ``False``.

        Returns:
            str: The SQL data type definition, e.g., ``'INT'``, ``'INT(11)'``,
            ``'INT UNSIGNED'``, or ``'INT(10) UNSIGNED ZEROFILL'``.

        Raises:
            ValueError: If the provided ``size`` is outside the allowed range for
                the chosen signed/unsigned mode.

        Example:
            Defining an INT column with a specific display width and unsigned flag::

                from ormophine.Mysql import DataTypes, TableStructure

                sql_type = DataTypes.INT(size=10, unsigned=True)
                # Returns: 'INT(10) UNSIGNED'

                # Using it in a table structure
                table = (TableStructure('products')
                        .add_column('stock', DataTypes.INT(unsigned=True, not_null=True)))
        """
        if size is not None:
            if (size < -2147483648 or size > 2147483647) and not unsigned or (size < 0 or size > 4294967295) and unsigned:
                raise ValueError("Size for INT must be between -2147483648 and 2147483647 for signed or 0 to 4294967295 for unsigned.")
        result = "INT"
        if size:
            result += f"({size})"
        if unsigned:
            result += " UNSIGNED"
        if zerofill:
            result += " ZEROFILL"
        return result

    @staticmethod
    def BIGINT(size: int = None, unsigned: bool = False, zerofill: bool = False) -> str:
        """
        Return the SQL data type string for a BIGINT column.

        BIGINT is a large integer type. The signed range is -2^63 to 2^63-1
        (-9223372036854775808 to 9223372036854775807), and the unsigned range is
        0 to 2^64-1 (18446744073709551615). The optional ``size`` parameter affects
        the display width but does not change the storage size or value range.
        The ``unsigned`` and ``zerofill`` flags modify the column definition.

        Args:
            size (int, optional): The display width of the column. If provided,
                it must be within the valid range for the chosen signed/unsigned
                mode. Defaults to ``None``, meaning the default display width is used.
            unsigned (bool, optional): If ``True``, the column is defined as
                ``UNSIGNED``, disallowing negative values. Defaults to ``False``.
            zerofill (bool, optional): If ``True``, the column is defined as
                ``ZEROFILL``, which pads displayed values with leading zeros up to
                the display width. Implies ``UNSIGNED``. Defaults to ``False``.

        Returns:
            str: The SQL data type definition, e.g., ``'BIGINT'``,
            ``'BIGINT(20)'``, or ``'BIGINT(20) UNSIGNED ZEROFILL'``.

        Raises:
            ValueError: If the provided ``size`` falls outside the allowed range
                for the chosen signed/unsigned mode. For signed, the allowed range
                is -9223372036854775808 to 9223372036854775807; for unsigned, it is
                0 to 18446744073709551615.

        Example:
            Defining a BIGINT column with a display width and unsigned flag::

                from ormophine.Mysql import DataTypes

                sql_type = DataTypes.BIGINT(size=20, unsigned=True)
                # Returns: 'BIGINT(20) UNSIGNED'

                # Using it in a table structure
                table = (TableStructure('orders')
                        .add_column('order_id', DataTypes.BIGINT(unsigned=True, not_null=True)))
        """
        if size is not None:
            if (size < -9223372036854775808 or size > 9223372036854775807) and not unsigned or (size < 0 or size > 18446744073709551615) and unsigned:
                raise ValueError("Size for BIGINT must be between -9223372036854775808 and 9223372036854775807 for signed or 0 to 18446744073709551615 for unsigned.")
        result = "BIGINT"
        if size:
            result += f"({size})"
        if unsigned:
            result += " UNSIGNED"
        if zerofill:
            result += " ZEROFILL"
        return result

    @staticmethod
    def DECIMAL(precision: int = 10, scale: int = 0) -> str:
        """
        Return the SQL data type string for a DECIMAL (exact fixed-point) column.

        DECIMAL is used to store exact numeric values with a fixed number of digits
        before and after the decimal point. It is ideal for financial and monetary
        data where precision is critical. The storage size depends on the precision
        and scale.

        Args:
            precision (int, optional): The total number of significant digits that
                can be stored, both to the left and right of the decimal point.
                Must be at least 1. Defaults to 10.
            scale (int, optional): The number of digits that can be stored after
                the decimal point. Must be between 0 and ``precision``. Defaults to 0.

        Returns:
            str: The SQL data type definition, e.g., ``'DECIMAL(10,2)'``.

        Raises:
            ValueError: If the provided ``precision`` is less than 1 or ``scale``
                is less than 0 or greater than ``precision`` (currently not enforced
                by the method but documented as the expected behavior).

        Example:
            Defining a DECIMAL column for product prices with 10 total digits and
            2 decimal places::

                from ormophine.Mysql import DataTypes, TableStructure

                price_type = DataTypes.DECIMAL(precision=10, scale=2)
                # Returns: 'DECIMAL(10,2)'

                table = (TableStructure('products')
                        .add_column('price', price_type, not_null=True))
        """
        if precision < 1 or scale < 0 or scale > precision:
            raise ValueError("Precision must be >= 1 and scale must be >= 0 and <= precision.")
        return f"DECIMAL({precision}, {scale})"

    @staticmethod
    def NUMERIC(precision: int = 10, scale: int = 0) -> str:
        """
        Return the SQL data type string for a NUMERIC column.

        NUMERIC is a synonym for DECIMAL in MySQL. It stores exact fixed-point
        numbers with a specified precision (total number of digits) and scale
        (number of digits after the decimal point). The precision must be at least
        1, and the scale must be between 0 and precision inclusive.

        Args:
            precision (int, optional): The total number of significant digits.
                Defaults to 10. Must be >= 1.
            scale (int, optional): The number of digits after the decimal point.
                Defaults to 0. Must be between 0 and ``precision`` inclusive.

        Returns:
            str: The SQL data type definition, e.g., ``'NUMERIC(10,2)'``.

        Raises:
            ValueError: If ``precision`` < 1, ``scale`` < 0, or ``scale`` > ``precision``.

        Example:
            Defining a NUMERIC column with 12 total digits and 4 decimal places::

                from ormophine.Mysql import DataTypes

                sql_type = DataTypes.NUMERIC(precision=12, scale=4)
                # Returns: 'NUMERIC(12, 4)'

                # Using it in a table structure
                table = (TableStructure('products')
                        .add_column('price', DataTypes.NUMERIC(10, 2), not_null=True))
        """
        if precision < 1 or scale < 0 or scale > precision:
            raise ValueError("Precision must be >= 1 and scale must be >= 0 and <= precision.")
        return f"NUMERIC({precision}, {scale})"

    @staticmethod
    def FLOAT(size: int = None, decimals: int = None) -> str:
        """
        Return the SQL data type string for a FLOAT column.

        FLOAT is a single‑precision floating‑point number. The optional ``size``
        and ``decimals`` parameters control the display width and the number of
        digits after the decimal point, respectively. If both are provided, the
        column is defined as ``FLOAT(size, decimals)``; otherwise, the bare
        ``FLOAT`` type is used.

        Note that the storage size and precision are determined by the MySQL
        implementation; specifying a size/decimals does not change the storage
        requirements but affects how values are displayed and parsed.

        Args:
            size (int, optional): The total number of digits (precision). If
                provided, must be a positive integer. Defaults to ``None``.
            decimals (int, optional): The number of digits after the decimal point
                (scale). If provided, must be a non‑negative integer. Defaults to
                ``None``. This parameter is only used when ``size`` is also given.

        Returns:
            str: The SQL data type definition, e.g., ``'FLOAT'`` or
            ``'FLOAT(7,4)'``.

        Raises:
            None: This method does not perform runtime validation of the arguments,
            though it is recommended to pass sensible values (positive integers).

        Example:
            Defining a FLOAT column with a custom precision and scale::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('measurements')
                        .add_column('temperature', DataTypes.FLOAT(5, 2)))
                # Generates: `temperature` FLOAT(5,2)

            Using the bare FLOAT type::

                table.add_column('humidity', DataTypes.FLOAT())
                # Generates: `humidity` FLOAT
        """
        if size is not None and decimals is not None:
            return f"FLOAT({size}, {decimals})"
        return "FLOAT"

    @staticmethod
    def DOUBLE(size: int = None, decimals: int = None) -> str:
        """
        Return the SQL data type string for a DOUBLE column.

        DOUBLE is a double‑precision floating‑point number (also known as ``REAL``
        in some contexts). The optional ``size`` and ``decimals`` parameters control
        the display width and the number of digits after the decimal point,
        respectively. If both are provided, the column is defined as
        ``DOUBLE(size, decimals)``; otherwise, the bare ``DOUBLE`` type is used.

        Note that the storage size and precision are determined by the MySQL
        implementation; specifying a size/decimals does not change the storage
        requirements but affects how values are displayed and parsed.

        Args:
            size (int, optional): The total number of digits (precision). If
                provided, must be a positive integer. Defaults to ``None``.
            decimals (int, optional): The number of digits after the decimal point
                (scale). If provided, must be a non‑negative integer. Defaults to
                ``None``. This parameter is only used when ``size`` is also given.

        Returns:
            str: The SQL data type definition, e.g., ``'DOUBLE'`` or
            ``'DOUBLE(10,4)'``.

        Raises:
            None: This method does not perform runtime validation of the arguments,
            though it is recommended to pass sensible values (positive integers for
            size and non‑negative integers for decimals).

        Example:
            Defining a DOUBLE column with a custom precision and scale::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('scientific_data')
                        .add_column('temperature', DataTypes.DOUBLE(8, 3)))
                # Generates: `temperature` DOUBLE(8,3)

            Using the bare DOUBLE type::

                table.add_column('humidity', DataTypes.DOUBLE())
                # Generates: `humidity` DOUBLE
        """
        if size is not None and decimals is not None:
            return f"DOUBLE({size}, {decimals})"
        return "DOUBLE"

    @staticmethod
    def REAL() -> str:
        """
        Return the SQL data type string for a REAL column.

        REAL is a synonym for ``DOUBLE`` (double-precision floating-point) in MySQL,
        though depending on the server SQL mode, it may be treated as ``FLOAT``
        (single-precision). In practice, MySQL treats ``REAL`` as ``DOUBLE`` by
        default, but this can be changed with the ``REAL_AS_FLOAT`` SQL mode.

        This method returns the exact string ``'REAL'``, leaving the interpretation
        to the MySQL server.

        Args:
            None

        Returns:
            str: The SQL data type definition, always ``'REAL'``.

        Raises:
            None

        Example:
            Using REAL in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('measurements')
                        .add_column('temperature', DataTypes.REAL()))
                # Generates: `temperature` REAL

            Note that the actual precision will depend on the server's SQL mode.
        """
        return "REAL"

    # ========================
    # String Data Types
    # ========================

    @staticmethod
    def CHAR(length: int = 255) -> str:
        """
        Return the SQL data type string for a fixed-length character column.

        ``CHAR`` stores fixed-length strings. Values shorter than the specified
        length are padded with spaces on the right. The maximum length is 255
        characters.

        Args:
            length (int, optional): The maximum number of characters the column
                can store. Must be between 1 and 255. Defaults to 255.

        Returns:
            str: The SQL data type definition, e.g., ``'CHAR(50)'``.

        Raises:
            ValueError: If ``length`` is less than 1 or greater than 255.

        Example:
            Defining a CHAR column with a specific length::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('countries')
                        .add_column('iso_code', DataTypes.CHAR(2), not_null=True))
                # Generates: `iso_code` CHAR(2) NOT NULL

            Using the default length (255)::

                table.add_column('description', DataTypes.CHAR())
                # Generates: `description` CHAR(255)
        """
        if length < 1 or length > 255:
            raise ValueError("Length for CHAR must be between 1 and 255.")
        return f"CHAR({length})"

    @staticmethod
    def VARCHAR(length: int = 255) -> str:
        """
        Return the SQL data type string for a VARCHAR column.

        VARCHAR is a variable‑length character string type. The maximum length is
        65,535 bytes, but the effective maximum may be less depending on the
        character set and row size limits. The specified ``length`` defines the
        maximum number of characters that can be stored.

        Args:
            length (int, optional): The maximum number of characters. Must be
                between 1 and 65535. Defaults to 255.

        Returns:
            str: The SQL data type definition, e.g., ``'VARCHAR(255)'``.

        Raises:
            ValueError: If ``length`` is not between 1 and 65535.

        Example:
            Defining a VARCHAR column for a user's name::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('full_name', DataTypes.VARCHAR(100)))
                # Generates: `full_name` VARCHAR(100)
        """
        if length < 1 or length > 65535:
            raise ValueError("Length for VARCHAR must be between 1 and 65535.")
        return f"VARCHAR({length})"

    @staticmethod
    def TINYTEXT() -> str:
        """
        Return the SQL data type string for a TINYTEXT column.

        TINYTEXT is a very small text column with a maximum length of 255 bytes
        (characters, depending on the character set). It is suitable for storing
        short strings such as titles, short descriptions, or small code snippets.
        Unlike VARCHAR, TINYTEXT has a fixed maximum size and is stored with a
        length prefix, making it efficient for very short data.

        Returns:
            str: The SQL data type definition, always ``'TINYTEXT'``.

        Example:
            Defining a TINYTEXT column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('articles')
                        .add_column('title', DataTypes.TINYTEXT()))
                # Generates: `title` TINYTEXT

            This is equivalent to using ``DataTypes.TEXT('TINYTEXT')`` but is
            provided as a convenient shorthand.
        """
        return "TINYTEXT"

    @staticmethod
    def TEXT(size: TEXT_SIZE = None) -> str:
        """
        Return the SQL data type string for a TEXT column.

        TEXT is a variable-length string type with a maximum length determined by
        the specified size variant. By default (when ``size`` is ``None``), this
        returns the standard ``TEXT`` type, which can store up to 65,535 characters.
        The optional ``size`` parameter allows specifying one of the four MySQL
        text size variants: ``'TINYTEXT'``, ``'TEXT'``, ``'MEDIUMTEXT'``, or
        ``'LONGTEXT'``.

        Args:
            size (TEXT_SIZE, optional): A string indicating the desired text size
                variant. Must be one of ``'TINYTEXT'``, ``'TEXT'``, ``'MEDIUMTEXT'``,
                or ``'LONGTEXT'``. Case-insensitive. Defaults to ``None``, which
                returns ``'TEXT'``.

        Returns:
            str: The SQL data type definition, e.g., ``'TEXT'``, ``'LONGTEXT'``,
            or ``'MEDIUMTEXT'``.

        Raises:
            ValueError: If the provided ``size`` is not one of the allowed values.

        Example:
            Defining a column using the default TEXT type::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('articles')
                        .add_column('content', DataTypes.TEXT()))
                # Generates: `content` TEXT

            Using a larger text type::

                table.add_column('long_description', DataTypes.TEXT('LONGTEXT'))
                # Generates: `long_description` LONGTEXT
        """
        if size:
            valid = {"TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}
            if size.upper() in valid:
                return size.upper()
            raise ValueError(f"Invalid TEXT size. Choose from {valid}")
        return "TEXT"

    @staticmethod
    def MEDIUMTEXT() -> str:
        """
        Return the SQL data type string for a MEDIUMTEXT column.

        MEDIUMTEXT is a variable-length string type capable of storing up to
        16,777,215 characters (approximately 16 MB). This is the medium-size
        variant in the MySQL TEXT family, larger than ``TEXT`` (65,535) and
        smaller than ``LONGTEXT`` (4,294,967,295). It is suitable for storing
        moderately large text data, such as articles, JSON documents, or logs.

        Unlike ``VARCHAR``, columns of type MEDIUMTEXT do not have a specified
        maximum length and are stored separately from the row data. They also
        cannot have default values (a restriction enforced by MySQL for all
        TEXT and BLOB types).

        Returns:
            str: The SQL data type definition, which is always the string
            ``'MEDIUMTEXT'``.

        Example:
            Creating a table with a MEDIUMTEXT column for storing detailed
            product descriptions::

                from ormophine.Mysql import DataTypes, TableStructure

                product_table = (TableStructure('products')
                                .add_column('id', DataTypes.INT(), primary_key=True,
                                            auto_increment=True, not_null=True)
                                .add_column('name', DataTypes.VARCHAR(100), not_null=True)
                                .add_column('description', DataTypes.MEDIUMTEXT()))
                # Generates: `description` MEDIUMTEXT

            The MEDIUMTEXT type is also useful for storing long-form content
            such as blog posts or comments::

                blog_table = (TableStructure('blog_posts')
                            .add_column('content', DataTypes.MEDIUMTEXT()))
                # `content` can hold up to ~16 MB of text
        """
        return "MEDIUMTEXT"

    @staticmethod
    def LONGTEXT() -> str:
        """
        Return the SQL data type string for a LONGTEXT column.

        LONGTEXT is the largest text type in MySQL, capable of storing up to
        4,294,967,295 characters (approximately 4 GB). It is suitable for very
        large text content such as extensive articles, logs, or JSON documents
        that exceed the capacity of ``MEDIUMTEXT``.

        Returns:
            str: The SQL data type definition, always ``'LONGTEXT'``.

        Example:
            Defining a column to store very large content::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('documents')
                        .add_column('content', DataTypes.LONGTEXT()))
                # Generates: `content` LONGTEXT
        """
        return "LONGTEXT"

    @staticmethod
    def BINARY(length: int = 1) -> str:
        """
        Return the SQL data type string for a BINARY column.

        BINARY is a fixed‑length binary string type. The ``length`` parameter
        specifies the number of bytes in the column; values shorter than this
        length are right‑padded with zero bytes (``\\x00``) when stored. The
        maximum allowed length is 255 bytes. If a value exceeds the defined
        length, it will be truncated or the database will raise an error
        depending on the SQL mode.

        Args:
            length (int, optional): The fixed byte length of the column. Must be
                between 1 and 255 (inclusive). Defaults to ``1``.

        Returns:
            str: The SQL data type definition, e.g., ``'BINARY(16)'`` or
            ``'BINARY(1)'``.

        Raises:
            None: This method does not perform runtime validation of the length;
                however, passing an invalid length (e.g., 0 or >255) will result
                in a database error when the column is created.

        Example:
            Defining a column to store a UUID (16 bytes) in binary format::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('uuid', DataTypes.BINARY(16)))
                # Generates: `uuid` BINARY(16)

            Using the default length of 1 byte::

                table.add_column('flag', DataTypes.BINARY())
                # Generates: `flag` BINARY(1)
        """
        return f"BINARY({length})"

    @staticmethod
    def VARBINARY(length: int = 255) -> str:
        """
        Return the SQL data type string for a VARBINARY column.

        VARBINARY is a variable‑length binary string type. It stores binary data
        (bytes) and is suitable for data that should not be interpreted as a
        character string. The maximum length is 65,535 bytes, and the default
        length is 255 bytes if not explicitly specified.

        Args:
            length (int, optional): The maximum number of bytes the column can
                store. Must be an integer between 1 and 65,535. Defaults to 255.

        Returns:
            str: The SQL data type definition, e.g., ``'VARBINARY(255)'`` or
            ``'VARBINARY(1000)'``.

        Raises:
            ValueError: If ``length`` is outside the allowed range (1–65535) –
                though this method does not perform validation itself; it is the
                caller's responsibility to pass a valid length.

        Example:
            Defining a VARBINARY column for storing hashed data (e.g., SHA‑256)::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('password_hash', DataTypes.VARBINARY(32)))
                # Generates: `password_hash` VARBINARY(32)

            Using the default length::

                table.add_column('binary_data', DataTypes.VARBINARY())
                # Generates: `binary_data` VARBINARY(255)
        """
        return f"VARBINARY({length})"

    @staticmethod
    def TINYBLOB() -> str:
        """
        Return the SQL data type string for a TINYBLOB column.

        TINYBLOB is a binary large object type that can store up to 255 bytes
        of binary data. It is suitable for very small binary content such as
        icons, tiny images, or short binary strings. Unlike text types, BLOB
        types store binary data without a character set or collation, making
        them ideal for non‑text data.

        This type is part of the MySQL BLOB family, which also includes BLOB,
        MEDIUMBLOB, and LONGBLOB for larger storage capacities.

        Returns:
            str: The SQL data type definition, exactly ``'TINYBLOB'``.

        Example:
            Defining a column for a small avatar image::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('avatar', DataTypes.TINYBLOB()))
                # Generates: `avatar` TINYBLOB
        """
        return "TINYBLOB"

    @staticmethod
    def BLOB(size: str = None) -> str:
        """
        Return the SQL data type string for a BLOB column.

        BLOB (Binary Large Object) is a variable-length binary string type with a
        maximum length determined by the specified size variant. By default (when
        ``size`` is ``None``), this returns the standard ``BLOB`` type, which can
        store up to 65,535 bytes. The optional ``size`` parameter allows specifying
        one of the four MySQL binary large object variants: ``'TINYBLOB'``,
        ``'BLOB'``, ``'MEDIUMBLOB'``, or ``'LONGBLOB'``.

        BLOB columns are used for storing binary data such as images, files, or
        serialized objects. They cannot have default values.

        Args:
            size (str, optional): A string indicating the desired BLOB size variant.
                Must be one of ``'TINYBLOB'``, ``'BLOB'``, ``'MEDIUMBLOB'``, or
                ``'LONGBLOB'``. (Note: the correct values are ``'TINYBLOB'``,
                ``'BLOB'``, ``'MEDIUMBLOB'``, and ``'LONGBLOB'``.) Case-insensitive.
                Defaults to ``None``, which returns ``'BLOB'``.

        Returns:
            str: The SQL data type definition, e.g., ``'BLOB'``, ``'LONGBLOB'``,
            or ``'MEDIUMBLOB'``.

        Raises:
            ValueError: If the provided ``size`` is not one of the allowed values.

        Example:
            Defining a BLOB column using the default size::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('files')
                        .add_column('data', DataTypes.BLOB()))
                # Generates: `data` BLOB

            Using a larger BLOB type::

                table.add_column('large_data', DataTypes.BLOB('LONGBLOB'))
                # Generates: `large_data` LONGBLOB
        """
        if size:
            valid = {"TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB"}
            if size.upper() in valid:
                return size.upper()
            raise ValueError(f"Invalid BLOB size. Choose from {valid}")
        return "BLOB"

    @staticmethod
    def MEDIUMBLOB() -> str:
        """
        Return the SQL data type string for a MEDIUMBLOB column.

        MEDIUMBLOB is a binary large object type that can store up to
        16,777,215 bytes (16 MiB). It is suitable for storing medium-sized
        binary data such as images, documents, or serialized objects.

        This type cannot have a default value.

        Returns:
            str: The SQL data type definition, exactly ``'MEDIUMBLOB'``.

        Example:
            Defining a MEDIUMBLOB column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('attachments')
                        .add_column('file_data', DataTypes.MEDIUMBLOB()))
                # Generates: `file_data` MEDIUMBLOB
        """
        return "MEDIUMBLOB"

    @staticmethod
    def LONGBLOB() -> str:
        """
        Return the SQL data type string for a LONGBLOB column.

        LONGBLOB is the largest binary large object type in MySQL, capable of
        storing up to 4,294,967,295 bytes (4 GiB). It is used for very large
        binary data such as videos, large files, or extensive binary blobs.

        Returns:
            str: The SQL data type definition ``'LONGBLOB'``.

        Example:
            Defining a LONGBLOB column for storing large binary files::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('videos')
                        .add_column('video_data', DataTypes.LONGBLOB()))
                # Generates: `video_data` LONGBLOB
        """
        return "LONGBLOB"

    @staticmethod
    def ENUM(*values: str) -> str:
        """
        Return the SQL data type string for an ENUM column.

        ENUM is a string object that can have only one value, chosen from a list of
        permitted values. The values are defined at column creation time and are
        stored as strings. This method constructs the ENUM definition by quoting
        and joining the provided values.

        The ENUM type is useful for columns that should only accept a limited set
        of predefined values, such as status fields or categories.

        Args:
            *values (str): Variable number of string values that define the
                permitted options for the ENUM. Each value will be quoted and
                separated by commas in the resulting SQL.

        Returns:
            str: The SQL data type definition, e.g., ``'ENUM('small', 'medium', 'large')'``.

        Example:
            Defining an ENUM column for product sizes::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('products')
                        .add_column('size', DataTypes.ENUM('small', 'medium', 'large')))
                # Generates: `size` ENUM('small','medium','large')

            Using ENUM for a status field with multiple options::

                table.add_column('status', DataTypes.ENUM('active', 'inactive', 'pending'))
                # Generates: `status` ENUM('active','inactive','pending')
        """
        quoted = ", ".join(f"'{v}'" for v in values)
        return f"ENUM({quoted})"

    @staticmethod
    def SET(*values: str) -> str:
        """
        Return the SQL data type string for a SET column.

        A SET is a string object that can store zero or more values from a
        predefined list of allowed values. Each value must be one of the
        provided strings. The column can store multiple values separated by
        commas, and the maximum number of distinct elements is 64.

        The list of allowed values is defined at table creation time and
        cannot be changed later. The order of values in the SET definition
        determines the internal numeric ordering.

        Args:
            *values (str): Variable number of string values that are permitted
                for this SET column. Each value must be a distinct string.
                The total number of values cannot exceed 64.

        Returns:
            str: The SQL data type definition, e.g., ``"SET('red','green','blue')"``.

        Raises:
            None: This method does not perform runtime validation, though it
                is recommended to ensure values do not contain commas or quotes,
                as they would need escaping.

        Example:
            Defining a SET column for storing favorite colors::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('favorite_colors', DataTypes.SET('red', 'green', 'blue')))
                # Generates: `favorite_colors` SET('red','green','blue')

            Inserting multiple values::

                db.users.insert({'favorite_colors': 'red,green'})
        """
        quoted = ", ".join(f"'{v}'" for v in values)
        return f"SET({quoted})"

    # ========================
    # Date and Time Data Types
    # ========================

    @staticmethod
    def DATE() -> str:
        """
        Return the SQL data type string for a DATE column.

        The DATE type stores a calendar date value in the format ``YYYY-MM-DD``.
        The supported range is from ``'1000-01-01'`` to ``'9999-12-31'`` in MySQL.
        This method returns the SQL string ``'DATE'``, suitable for use in column
        definitions.

        Returns:
            str: The SQL data type definition ``'DATE'``.

        Example:
            Defining a DATE column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('events')
                        .add_column('event_date', DataTypes.DATE()))
                # Generates: `event_date` DATE
        """
        return "DATE"

    @staticmethod
    def TIME(precision: int = None) -> str:
        """
        Return the SQL data type string for a TIME column.

        TIME represents a time value in the format ``HH:MM:SS``. An optional
        ``precision`` parameter can be specified to enable fractional seconds
        (microsecond precision) with a value between 0 and 6. If no precision is
        given, the standard ``TIME`` type is returned.

        Args:
            precision (int, optional): The number of digits for fractional seconds.
                Must be an integer between 0 and 6 inclusive. If provided, the
                returned type includes the precision in parentheses, e.g.,
                ``TIME(3)``. Defaults to ``None``, which returns the bare ``TIME``
                type.

        Returns:
            str: The SQL data type definition, e.g., ``'TIME'`` or ``'TIME(3)'``.

        Raises:
            None: This method does not perform runtime validation of the precision
                value, though it is recommended to pass values in the valid range.

        Example:
            Defining a TIME column with microsecond precision::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('events')
                        .add_column('start_time', DataTypes.TIME(3)))
                # Generates: `start_time` TIME(3)

            Using the default TIME type without fractional seconds::

                table.add_column('duration', DataTypes.TIME())
                # Generates: `duration` TIME
        """
        if precision is not None:
            return f"TIME({precision})"
        return "TIME"

    @staticmethod
    def DATETIME(precision: int = None) -> str:
        """
        Return the SQL data type string for a DATETIME column.

        DATETIME represents a date and time combination in the format
        ``YYYY-MM-DD HH:MM:SS``. An optional ``precision`` parameter can be
        specified to enable fractional seconds (microsecond precision) with a
        value between 0 and 6. If no precision is given, the standard
        ``DATETIME`` type is returned.

        Args:
            precision (int, optional): The number of digits for fractional seconds.
                Must be an integer between 0 and 6 inclusive. If provided, the
                returned type includes the precision in parentheses, e.g.,
                ``DATETIME(3)``. Defaults to ``None``, which returns the bare
                ``DATETIME`` type.

        Returns:
            str: The SQL data type definition, e.g., ``'DATETIME'`` or
            ``'DATETIME(3)'``.

        Raises:
            None: This method does not perform runtime validation of the precision
                value, though it is recommended to pass values in the valid range.

        Example:
            Defining a DATETIME column with microsecond precision::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('logs')
                        .add_column('created_at', DataTypes.DATETIME(6)))
                # Generates: `created_at` DATETIME(6)

            Using the default DATETIME type without fractional seconds::

                table.add_column('updated_at', DataTypes.DATETIME())
                # Generates: `updated_at` DATETIME
        """
        if precision is not None:
            return f"DATETIME({precision})"
        return "DATETIME"

    @staticmethod
    def TIMESTAMP(precision: int = None) -> str:
        """
        Return the SQL data type string for a TIMESTAMP column.

        TIMESTAMP represents a date and time combination in the format
        ``YYYY-MM-DD HH:MM:SS``, with a range from ``1970-01-01 00:00:01`` UTC to
        ``2038-01-19 03:14:07`` UTC. An optional ``precision`` parameter can be
        specified to enable fractional seconds (microsecond precision) with a value
        between 0 and 6. If no precision is given, the standard ``TIMESTAMP`` type
        is returned.

        Args:
            precision (int, optional): The number of digits for fractional seconds.
                Must be an integer between 0 and 6 inclusive. If provided, the
                returned type includes the precision in parentheses, e.g.,
                ``TIMESTAMP(3)``. Defaults to ``None``, which returns the bare
                ``TIMESTAMP`` type.

        Returns:
            str: The SQL data type definition, e.g., ``'TIMESTAMP'`` or
            ``'TIMESTAMP(6)'``.

        Raises:
            None: This method does not perform runtime validation of the precision
                value, though it is recommended to pass values in the valid range.

        Example:
            Defining a TIMESTAMP column with microsecond precision::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('logs')
                        .add_column('created_at', DataTypes.TIMESTAMP(3)))
                # Generates: `created_at` TIMESTAMP(3)

            Using the default TIMESTAMP type without fractional seconds::

                table.add_column('updated_at', DataTypes.TIMESTAMP())
                # Generates: `updated_at` TIMESTAMP
        """
        if precision is not None:
            return f"TIMESTAMP({precision})"
        return "TIMESTAMP"

    @staticmethod
    def YEAR() -> str:
        """
        Return the SQL data type string for a YEAR column.

        The YEAR type represents a year value in the range 1901 to 2155, or
        0000. It is stored as a 1‑byte integer and is displayed in the format
        ``YYYY``. This type is commonly used for storing year‑only data such as
        birth years, model years, or fiscal years.

        Returns:
            str: The SQL data type definition, always ``'YEAR'``.

        Example:
            Defining a YEAR column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('products')
                        .add_column('release_year', DataTypes.YEAR()))
                # Generates: `release_year` YEAR

            The YEAR type does not accept any parameters; the returned string
            is always just ``'YEAR'``.
        """
        return "YEAR"

    # ========================
    # Spatial Data Types
    # ========================

    @staticmethod
    def GEOMETRY() -> str:
        """
        Return the SQL data type string for a GEOMETRY column.

        GEOMETRY is a spatial data type that can store any kind of geometry object,
        such as points, line strings, polygons, or collections thereof. It is the
        base type for all spatial types in MySQL and can be used to store spatial
        data in a generic way.

        When using this type, the column can hold any valid geometry value. For
        more specific spatial types, consider using :meth:`POINT`, :meth:`LINESTRING`,
        :meth:`POLYGON`, or the collection types.

        Returns:
            str: The SQL data type definition, always ``'GEOMETRY'``.

        Example:
            Defining a GEOMETRY column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('locations')
                        .add_column('geo_data', DataTypes.GEOMETRY()))
                # Generates: `geo_data` GEOMETRY

            Using it with a spatial index (not directly supported by the ORM, but
            can be added via custom SQL)::

                # The column can store points, lines, polygons, etc.
                db.custom_execute(
                    "CREATE SPATIAL INDEX idx_geo ON locations(geo_data);"
                )
        """
        return "GEOMETRY"

    @staticmethod
    def POINT() -> str:
        """
        Return the SQL data type string for a POINT column.

        POINT is a spatial data type representing a point in two-dimensional space
        (X and Y coordinates). It is part of MySQL's geometry type family and can
        be used with spatial indexes and functions for geographic or geometric
        calculations.

        Returns:
            str: The SQL data type definition, always ``'POINT'``.

        Example:
            Defining a POINT column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('locations')
                        .add_column('coordinates', DataTypes.POINT()))
                # Generates: `coordinates` POINT

            This column can then be used with MySQL's spatial functions like
            ``ST_Contains``, ``ST_Distance``, etc.
        """
        return "POINT"

    @staticmethod
    def LINESTRING() -> str:
        """
        Return the SQL data type string for a LINESTRING column.

        LINESTRING is a spatial data type representing a curve with linear
        interpolated points. It is used in geographic information systems (GIS)
        to store line geometries, such as roads, rivers, or routes. This method
        returns the bare ``LINESTRING`` type definition without any additional
        parameters.

        Returns:
            str: The SQL data type definition, always ``'LINESTRING'``.

        Example:
            Defining a LINESTRING column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('routes')
                        .add_column('path', DataTypes.LINESTRING()))
                # Generates: `path` LINESTRING
        """
        return "LINESTRING"

    @staticmethod
    def POLYGON() -> str:
        """
        Return the SQL data type string for a POLYGON column.

        POLYGON is a spatial data type representing a closed planar shape defined
        by a set of points forming a boundary. It is commonly used in geographic
        information systems (GIS) to store areas such as countries, lakes, or
        property boundaries. A polygon consists of at least one linear ring (a
        closed loop) and may contain interior rings (holes). This method returns
        the bare ``POLYGON`` type definition without any additional parameters.

        Returns:
            str: The SQL data type definition, always ``'POLYGON'``.

        Example:
            Defining a POLYGON column in a table structure for storing geographic
            areas::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('geographic_areas')
                        .add_column('boundary', DataTypes.POLYGON()))
                # Generates: `boundary` POLYGON
        """
        return "POLYGON"

    @staticmethod
    def MULTIPOINT() -> str:
        """
        Return the SQL data type string for a MULTIPOINT column.

        MULTIPOINT is a spatial data type representing a collection of zero or more
        :class:`POINT` geometries. It is used in geographic information systems
        (GIS) to store multiple point locations, such as clusters of landmarks or
        sensor positions. This method returns the bare ``MULTIPOINT`` type
        definition without any additional parameters.

        Returns:
            str: The SQL data type definition, always ``'MULTIPOINT'``.

        Example:
            Defining a MULTIPOINT column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('sensor_networks')
                        .add_column('locations', DataTypes.MULTIPOINT()))
                # Generates: `locations` MULTIPOINT
        """
        return "MULTIPOINT"

    @staticmethod
    def MULTILINESTRING() -> str:
        """
        Return the SQL data type string for a MULTILINESTRING column.

        MULTILINESTRING is a spatial data type that represents a collection of
        one or more :class:`LINESTRING` geometries. It is used in geographic
        information systems (GIS) to store multiple line features, such as road
        networks, river systems, or other compound linear geometries. This method
        returns the bare ``MULTILINESTRING`` type definition without additional
        parameters.

        Returns:
            str: The SQL data type definition, always ``'MULTILINESTRING'``.

        Example:
            Defining a MULTILINESTRING column in a table structure for storing
            multiple route paths::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('road_networks')
                        .add_column('routes', DataTypes.MULTILINESTRING()))
                # Generates: `routes` MULTILINESTRING
        """
        return "MULTILINESTRING"

    @staticmethod
    def MULTIPOLYGON() -> str:
        """
        Return the SQL data type string for a MULTIPOLYGON column.

        MULTIPOLYGON is a spatial data type representing a collection of polygons
        in a geographic information system (GIS). It can store multiple polygon
        geometries as a single value, useful for representing complex regions
        such as countries with islands, administrative boundaries, or multi-part
        land parcels. This method returns the bare ``MULTIPOLYGON`` type definition
        without any additional parameters.

        Returns:
            str: The SQL data type definition, always ``'MULTIPOLYGON'``.

        Example:
            Defining a MULTIPOLYGON column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('countries')
                        .add_column('boundary', DataTypes.MULTIPOLYGON()))
                # Generates: `boundary` MULTIPOLYGON
        """
        return "MULTIPOLYGON"

    @staticmethod
    def GEOMETRYCOLLECTION() -> str:
        """
        Return the SQL data type string for a GEOMETRYCOLLECTION column.

        GEOMETRYCOLLECTION is a spatial data type that can store a collection of
        mixed geometry types, such as points, linestrings, and polygons, all within
        a single column. It is useful for representing complex geographic features
        that consist of multiple geometric shapes. This method returns the bare
        ``GEOMETRYCOLLECTION`` type definition without any additional parameters.

        Returns:
            str: The SQL data type definition, always ``'GEOMETRYCOLLECTION'``.

        Example:
            Defining a GEOMETRYCOLLECTION column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('geospatial_data')
                        .add_column('mixed_shapes', DataTypes.GEOMETRYCOLLECTION()))
                # Generates: `mixed_shapes` GEOMETRYCOLLECTION
        """
        return "GEOMETRYCOLLECTION"

    # ========================
    # JSON Data Type
    # ========================

    @staticmethod
    def JSON() -> str:
        """
        Return the SQL data type string for a JSON column.

        This method generates the native JSON data type introduced in MySQL 5.7.
        JSON columns store JSON (JavaScript Object Notation) documents and provide
        automatic validation of JSON data. They support efficient indexing and
        querying of JSON values using JSON functions and operators.

        Returns:
            str: The SQL data type definition, always ``'JSON'``.

        Example:
            Defining a JSON column in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('settings')
                        .add_column('configuration', DataTypes.JSON()))
                # Generates: `configuration` JSON
        """
        return "JSON"

    # ========================
    # Special / Other
    # ========================

    @staticmethod
    def SERIAL() -> str:
        """
        Return the SQL data type string for a SERIAL column.

        SERIAL is a MySQL alias for ``BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE``.
        It is a convenience shorthand commonly used for auto‑incrementing primary keys.
        The type provides a large integer range (0 to 2⁶⁴‑1) and automatically generates
        a unique, non‑null value for each inserted row.

        Returns:
            str: The SQL data type definition, always ``'SERIAL'``.

        Example:
            Using SERIAL as an auto‑increment primary key in a table structure::

                from ormophine.Mysql import DataTypes, TableStructure

                table = (TableStructure('users')
                        .add_column('id', DataTypes.SERIAL(), primary_key=True,
                                    auto_increment=True, not_null=True))
                # Generates: `id` SERIAL

            Note that when using ``DataTypes.SERIAL()``, you typically set
            ``primary_key=True``, ``auto_increment=True``, and ``not_null=True``
            explicitly, or rely on the convenience method in your table builder.
        """
        return "SERIAL"

    @staticmethod
    def BOOLEAN() -> str:
        """Returns the SQL BOOLEAN type string for true/false values.

        ``BOOLEAN`` stores a logical truth value (``TRUE``, ``FALSE``, or
        ``NULL``). In MySQL/MariaDB, ``BOOLEAN`` is a synonym for
        ``TINYINT(1)``: values ``0`` and ``1`` are stored internally, while
        the literal keywords ``TRUE``/``FALSE`` are accepted in SQL.

        Returns:
            str: The string ``"BOOLEAN"``, ready for use in a column
            definition.

        Example:
            >>> from ormophine.Mysql import DataTypes, TableStructure
            >>> structure = TableStructure("users")
            >>> structure.add_column("is_active", DataTypes.BOOLEAN(),
            ...                      default=True, not_null=True)
        """
        return "BOOLEAN"        

class TableStructure:
    """
    A builder class for constructing MySQL table definitions.

    This class provides a fluent interface for defining columns, primary keys,
    foreign keys, and table-level options (engine, charset, collation). Once
    the structure is fully defined, the :meth:`get_structure` method generates
    a complete ``CREATE TABLE`` SQL statement that can be executed via the
    :class:`Driver` or directly on a database connection.

    The builder pattern allows method chaining (e.g., ``TableStructure(...)
    .add_column(...).add_column(...).foreign_key(...)``) for concise and
    readable table definitions. All columns are stored internally and validated
    for consistency before generating the final SQL.

    Attributes:
        table_query (str): The internal SQL fragment containing column and
            constraint definitions (comma‑separated).
        primary_keys (list): List of column names (with backticks) that form
            the primary key.
        items (dict): Internal storage mapping column names to their properties
            (datatype, default, unique, not_null, primary_key, auto_increment).
        name (str): The table name, enclosed in backticks.
        foreigns (list): List of foreign key constraint fragments.
        charset (str): The character set for the table (e.g., ``'utf8mb4'``).
        collate (str): The collation for the table (e.g., ``'utf8mb4_bin'``).

    Args:
        table_name (str): The name of the table to be created.
        charset (CHARSET, optional): The character set for the table.
            Defaults to ``'utf8mb4'``.
        collate (COLLATE, optional): The collation for the table.
            Defaults to ``'utf8mb4_bin'``.

    Raises:
        Exception: If validation fails during column addition (e.g., duplicate
            column name, invalid AUTO_INCREMENT usage, or conflicting constraints).
        Exception: If :meth:`get_structure` is called with no columns defined.

    Example:
        Creating a table structure for a ``users`` table with various column types
        and constraints, then using it with a :class:`Driver` instance::

            from ormophine.Mysql import TableStructure, DataTypes, Driver

            # Build the table definition
            users_table = (TableStructure('users', charset='utf8mb4')
                           .add_column('id', DataTypes.INT(),
                                       primary_key=True,
                                       auto_increment=True,
                                       not_null=True)
                           .add_column('username', DataTypes.VARCHAR(50),
                                       not_null=True,
                                       unique=True)
                           .add_column('email', DataTypes.VARCHAR(255),
                                       not_null=True)
                           .add_column('age', DataTypes.TINYINT(),
                                       default_value=0)
                           .add_column('bio', DataTypes.TEXT())
                           .add_column('created_at', DataTypes.DATETIME(),
                                       default_value='CURRENT_TIMESTAMP')
                           .foreign_key('email', 'profiles', 'user_email',
                                        on_delete='CASCADE'))

            # Connect to the database and create the table
            db = Driver(host='localhost', port=3306, username='root',
                        password='secret', db_name='myapp')
            db.create_table(users_table)

            # The table is now available as an attribute on the driver
            db.users.insert({'username': 'alice', 'email': 'alice@example.com'})

    Note:
        The class is designed for MySQL and uses MySQL-specific syntax.
        It does not support all advanced features like CHECK constraints or
        partitions, but covers the most common DDL operations.
    """
    ON_ACTION= Literal['CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION']
    CHARSET = Literal[
    "armscii8",
    "ascii",
    "big5",
    "binary",
    "cp1250",
    "cp1251",
    "cp1256",
    "cp1257",
    "cp850",
    "cp852",
    "cp866",
    "cp932",
    "dec8",
    "eucjpms",
    "euckr",
    "gb18030",
    "gb2312",
    "gbk",
    "geostd8",
    "greek",
    "hebrew",
    "hp8",
    "keybcs2",
    "koi8r",
    "koi8u",
    "latin1",
    "latin2",
    "latin5",
    "latin7",
    "macce",
    "macroman",
    "sjis",
    "swe7",
    "tis620",
    "ucs2",
    "ujis",
    "utf16",
    "utf16le",
    "utf32",
    "utf8mb3",
    "utf8mb4"
    ]
    COLLATE = Literal[
    "utf8mb4_0900_ai_ci",
    "utf8mb4_0900_as_cs",
    "utf8mb4_0900_bin",
    "utf8mb4_general_ci",
    "utf8mb4_unicode_ci",
    "utf8mb4_unicode_520_ci",
    "utf8mb4_bin",
    "utf8mb4_persian_ci",
    "utf8mb4_ar_0900_ai_ci",
    "utf8mb4_da_0900_ai_ci",
    "utf8mb4_de_pb_0900_ai_ci",
    "utf8mb4_en_0900_ai_ci",
    "utf8mb4_es_0900_ai_ci",
    "utf8mb4_es_trad_0900_ai_ci",
    "utf8mb4_fr_0900_ai_ci",
    "utf8mb4_it_0900_ai_ci",
    "utf8mb4_nl_0900_ai_ci",
    "utf8mb4_pt_0900_ai_ci",
    "utf8mb4_cs_0900_ai_ci",
    "utf8mb4_hr_0900_ai_ci",
    "utf8mb4_hu_0900_ai_ci",
    "utf8mb4_pl_0900_ai_ci",
    "utf8mb4_ro_0900_ai_ci",
    "utf8mb4_sk_0900_ai_ci",
    "utf8mb4_sl_0900_ai_ci",
    "utf8mb4_sv_0900_ai_ci",
    "utf8mb4_nb_0900_ai_ci",
    "utf8mb4_nn_0900_ai_ci",
    "utf8mb4_is_0900_ai_ci",
    "utf8mb4_lt_0900_ai_ci",
    "utf8mb4_lv_0900_ai_ci",
    "utf8mb4_et_0900_ai_ci",
    "utf8mb4_bg_0900_ai_ci",
    "utf8mb4_sr_latn_0900_ai_ci",
    "utf8mb4_bs_0900_ai_ci",
    "utf8mb4_mk_0900_ai_ci",
    "utf8mb4_ja_0900_as_cs",
    "utf8mb4_ko_0900_as_cs",
    "utf8mb4_zh_0900_as_cs",
    "utf8mb4_tr_0900_ai_ci",
    "utf8mb4_vi_0900_ai_ci",
    "utf8mb4_0900_as_cs",
    "utf8mb4_da_0900_as_cs",
    "utf8mb4_es_0900_as_cs",
    "utf8mb4_fr_0900_as_cs",
    "utf8mb4_it_0900_as_cs",
    "utf8mb4_ja_0900_as_cs",
    "utf8mb4_ko_0900_as_cs",
    "utf8mb4_zh_0900_as_cs",
    "utf8mb4_croatian_ci",
    "utf8mb4_czech_ci",
    "utf8mb4_danish_ci",
    "utf8mb4_esperanto_ci",
    "utf8mb4_estonian_ci",
    "utf8mb4_german2_ci",
    "utf8mb4_hungarian_ci",
    "utf8mb4_icelandic_ci",
    "utf8mb4_latvian_ci",
    "utf8mb4_lithuanian_ci",
    "utf8mb4_polish_ci",
    "utf8mb4_romanian_ci",
    "utf8mb4_slovak_ci",
    "utf8mb4_slovenian_ci",
    "utf8mb4_swedish_ci",
    "utf8mb4_turkish_ci"
    ]

    def __init__(self, table_name: str, charset: CHARSET = "utf8mb4", collate: COLLATE = "utf8mb4_bin"):
        """
        Initialize a new table structure builder for creating a MySQL table.

        The :class:`TableStructure` class is used to programmatically define a table's
        columns, constraints, foreign keys, and table‑level options. This constructor
        sets the table name, character set, and collation. Columns are added via
        :meth:`add_column`, and the final ``CREATE TABLE`` statement is generated
        by :meth:`get_structure`.

        Args:
            table_name (str): The name of the table to be created. It will be
                quoted with backticks internally.
            charset (CHARSET, optional): The default character set for the table.
                Must be one of the valid MySQL character set names (e.g.,
                ``'utf8mb4'``, ``'latin1'``). Defaults to ``'utf8mb4'``.
            collate (COLLATE, optional): The default collation for the table.
                Must be one of the valid MySQL collation names (e.g.,
                ``'utf8mb4_bin'``, ``'utf8mb4_general_ci'``). Defaults to
                ``'utf8mb4_bin'``.

        Returns:
            None

        Example:
            Building a simple table structure::

                from ormophine.Mysql import TableStructure, DataTypes

                users = (TableStructure('users', charset='utf8mb4', collate='utf8mb4_unicode_ci')
                        .add_column('id', DataTypes.INT(), primary_key=True, auto_increment=True, not_null=True)
                        .add_column('username', DataTypes.VARCHAR(50), not_null=True, unique=True)
                        .add_column('created_at', DataTypes.DATETIME(), default_value='CURRENT_TIMESTAMP'))
        """
        self.table_query= ''
        self.primary_keys= []
        self.items= {}
        self.name= f'`{table_name}`'
        self.foreigns= []
        self.charset = charset
        self.collate = collate

    def _validate_column(
        self,
        column_name,
        datatype,
        default_value,
        unique,
        not_null,
        primary_key,
        auto_increment
    ):
        """
        Validate column definition parameters before adding a column.

        This internal method performs comprehensive validation of column properties
        to ensure they are consistent with MySQL rules. It checks data type validity,
        primary key constraints, uniqueness, auto_increment rules, default value
        compatibility, and other logical constraints. If any validation fails, a
        descriptive exception is raised.

        Args:
            column_name (str): The name of the column being validated (already
                backtick-quoted).
            datatype (str): The SQL data type string (e.g., returned by
                :class:`DataTypes` methods).
            default_value (Any): The default value for the column, or ``None``.
            unique (bool): Whether the column should have a UNIQUE constraint.
            not_null (bool): Whether the column is NOT NULL.
            primary_key (bool): Whether the column is a PRIMARY KEY.
            auto_increment (bool): Whether the column has AUTO_INCREMENT.

        Returns:
            None: This method does not return a value; it raises exceptions on
            validation failure.

        Raises:
            TypeError: If ``datatype`` is not a string.
            Exception: For various validation errors, including:
                - PRIMARY KEY columns cannot also be UNIQUE.
                - Column name already exists in the table definition.
                - Bytes objects cannot be used as default values.
                - Only one AUTO_INCREMENT column is allowed per table.
                - AUTO_INCREMENT is only allowed on numeric columns.
                - AUTO_INCREMENT column must be PRIMARY KEY or UNIQUE.
                - AUTO_INCREMENT columns cannot have DEFAULT values.
                - TEXT and BLOB columns cannot have default values.
                - SERIAL implies PRIMARY KEY, AUTO_INCREMENT, and NOT NULL.

        Example:
            This method is called internally by :meth:`add_column` and should not
            typically be used directly::

                # Internal usage within TableStructure
                table_structure._validate_column(
                    column_name='`id`',
                    datatype='INT',
                    default_value=None,
                    unique=False,
                    not_null=True,
                    primary_key=True,
                    auto_increment=True
                )
                # Validation passes

                # Invalid: PRIMARY KEY with UNIQUE
                # Raises Exception: "PRIMARY KEY columns cannot be UNIQUE"
        """
        if not isinstance(datatype, str):
            raise TypeError("datatype must be a string returned by DataTypes.")

        if primary_key:
            if unique:
                raise Exception("PRIMARY KEY columns cannot be UNIQUE, as they are inherently unique.")
                
        if column_name in self.items:
            raise Exception(f"Column {column_name} already exists.")
                
        if isinstance(default_value, bytes):
            raise Exception("Bytes objects cannot be used as default values.")
                
        for values in self.items.values():
            if values[5] and auto_increment:
                raise Exception("Only one AUTO_INCREMENT column is allowed.")

        numeric = (
            "BIT",
            "TINYINT",
            "SMALLINT",
            "MEDIUMINT",
            "INT",
            "BIGINT",
            "DECIMAL",
            "NUMERIC",
            "FLOAT",
            "DOUBLE",
            "REAL"
        )

        if auto_increment:
            if datatype.split("(")[0].split()[0] not in numeric:
                raise Exception("AUTO_INCREMENT is only allowed on numeric columns.")
            if not (primary_key or unique):
                raise Exception("AUTO_INCREMENT column must be PRIMARY KEY or UNIQUE.")
            if default_value is not None:
                raise Exception("AUTO_INCREMENT columns cannot have DEFAULT values.")
            
        if 'TEXT' in datatype or 'BLOB' in datatype:
            if default_value is not None:
                raise Exception("TEXT and BLOB columns cannot have default values.")
            
    def add_column(self, column_name: str, datatype: DataTypes,
                    default_value=None, unique: bool = None,
                    not_null: bool = None,
                    primary_key: bool = None,
                    auto_increment: bool = False):
        """
        Add a column definition to the table structure.

        This method defines a new column with the given name and data type,
        along with optional constraints (default, unique, not null, primary key,
        auto-increment). The column is added to the internal representation of
        the table; the actual SQL is generated later by :meth:`get_structure`.
        The method performs comprehensive validation of the column options to
        ensure they are consistent with MySQL rules.

        If the ``datatype`` is ``'SERIAL'``, the method automatically sets
        ``primary_key``, ``not_null``, and ``auto_increment`` to ``True``,
        overriding any provided values. This follows MySQL's ``SERIAL`` alias
        behavior.

        Args:
            column_name (str): The name of the column. It will be sanitized and
                quoted as an identifier (backticks added).
            datatype (DataTypes): A string returned by one of the :class:`DataTypes`
                static methods, e.g., ``DataTypes.INT()`` or ``DataTypes.VARCHAR(255)``.
            default_value (Any, optional): The default value for the column. If a
                string, it will be quoted in the SQL. Bytes objects are not allowed.
                Defaults to ``None``.
            unique (bool, optional): If ``True``, adds a ``UNIQUE`` constraint.
                Defaults to ``None`` (no constraint). Cannot be used with ``primary_key``.
            not_null (bool, optional): If ``True``, adds a ``NOT NULL`` constraint.
                Defaults to ``None``. Must be ``True`` for primary keys.
            primary_key (bool, optional): If ``True``, designates the column as a
                primary key. Implies ``not_null`` and uniqueness. Defaults to ``None``.
            auto_increment (bool, optional): If ``True``, enables auto-increment.
                Only allowed on numeric columns and requires the column to be a
                primary key or unique. Defaults to ``False``.

        Returns:
            TableStructure: The current instance, allowing method chaining.

        Raises:
            TypeError: If ``datatype`` is not a string.
            Exception: If any validation rule is violated, such as:
                - PRIMARY KEY column not NOT NULL.
                - PRIMARY KEY column also marked UNIQUE.
                - Duplicate column name.
                - Default value is a bytes object.
                - More than one AUTO_INCREMENT column defined.
                - AUTO_INCREMENT on a non-numeric column.
                - AUTO_INCREMENT without PRIMARY KEY or UNIQUE.
                - AUTO_INCREMENT with a default value.
                - DEFAULT on TEXT or BLOB columns.
                - SERIAL used without PRIMARY KEY, AUTO_INCREMENT, and NOT NULL.

        Example:
            Building a table structure with columns::

                from ormophine.Mysql import TableStructure, DataTypes

                table = (TableStructure('users')
                        .add_column('id', DataTypes.INT(), primary_key=True,
                                    auto_increment=True, not_null=True)
                        .add_column('username', DataTypes.VARCHAR(50),
                                    unique=True, not_null=True)
                        .add_column('age', DataTypes.TINYINT(), default_value=0)
                        .add_column('bio', DataTypes.TEXT()))
                # The structure is now ready for get_structure()
        """
        column_name = f'`{column_name.strip()}`'
        auto_increment , not_null, unique = (False, False, False) if 'SERIAL' in datatype else (auto_increment, not_null, unique)
        self._validate_column(column_name,datatype,default_value,unique,not_null,primary_key,auto_increment)
        for item in self.table_query.split(','):
            if item and (column_name in item) and item.split(' ')[1] == column_name:
                raise Exception('You have added this column befor\nif you wanna modify this column , delete this column and then add a new one with desired options')
        if type(default_value) == bytes:
            raise Exception('Cant set bytes object as default value')
        self.primary_keys.append(column_name) if primary_key else None
        self.items[column_name] = [datatype, default_value, unique, not_null, primary_key, auto_increment]
        self.table_query = self.table_query + f' {column_name.strip()} {datatype}{" AUTO_INCREMENT" if auto_increment else ""}{" UNIQUE" if unique else ""}{" NOT NULL" if not_null else ""}{f" DEFAULT {('TRUE' if default_value else 'FALSE') if isinstance(default_value,bool) else f"'{default_value}'" if type(default_value) == str else str(default_value)}" if default_value is not None else ""},'
        return self

    def get_columns(self):
        """
        Retrieve a list of column definitions from the current table structure.

        This method returns a list of dictionaries, each containing detailed
        information about a column that has been added to the table via
        :meth:`add_column`. The returned data reflects the current state of
        the internal structure and can be used for inspection or to generate
        the final ``CREATE TABLE`` statement.

        Returns:
            list of dict: A list where each element is a dictionary with the
            following keys:

            - ``name`` (str): The column name, including backticks (e.g., ``'`id`'``).
            - ``datatype`` (str): The SQL data type string (e.g., ``'INT'``).
            - ``default_value`` (Any): The default value for the column, or
            ``None`` if not set.
            - ``unique`` (bool): ``True`` if the column has a ``UNIQUE``
            constraint, otherwise ``False``.
            - ``not_null`` (bool): ``True`` if the column is ``NOT NULL``,
            otherwise ``False``.
            - ``primari_key`` (bool): ``True`` if the column is part of the
            primary key, otherwise ``False``. (Note the typo in the key name
            which is preserved from the original implementation.)

        Raises:
            None: This method does not raise any exceptions.

        Example:
            Building a table structure and retrieving its column definitions::

                from ormophine.Mysql import TableStructure, DataTypes

                struct = (TableStructure('users')
                        .add_column('id', DataTypes.INT(), primary_key=True,
                                    auto_increment=True, not_null=True)
                        .add_column('name', DataTypes.VARCHAR(100),
                                    not_null=True)
                        .add_column('age', DataTypes.INT(), default_value=0))

                columns = struct.get_columns()
                for col in columns:
                    print(f"{col['name']}: {col['datatype']} "
                        f"(PK: {col['primari_key']}, Not Null: {col['not_null']})")
                # Output:
                # `id`: INT (PK: True, Not Null: True)
                # `name`: VARCHAR(100) (PK: False, Not Null: True)
                # `age`: INT (PK: False, Not Null: False)
        """
        items_list = []
        for item in self.items:
            items_dict = {}
            values = self.items[item]
            items_dict['name']= item
            items_dict['datatype']= values[0]
            items_dict['default_value']= values[1]
            items_dict['unique']= True if values[2] else False
            items_dict['not_null']= True if values[3] else False
            items_dict['primari_key']= True if values[4] else False
            items_list.append(items_dict)
        return items_list

    def foreign_key(self, column: str, refrences_table: 'Table',
                    refrences_column: 'Column', on_delete: ON_ACTION = None,
                    on_update: ON_ACTION = None
                    ):
        """
        Add a foreign key constraint to the table structure.

        This method defines a foreign key relationship between a column in the
        current table and a column in a referenced (parent) table. The constraint
        ensures referential integrity by enforcing that values in the foreign key
        column match existing values in the referenced column. Optional ``ON DELETE``
        and ``ON UPDATE`` actions can be specified to control behavior when the
        referenced row is deleted or updated.

        The constraint is added to the internal list of foreign keys and will be
        included in the final ``CREATE TABLE`` statement generated by
        :meth:`get_structure`. Multiple foreign keys can be added to the same table.

        Args:
            column (str): The name of the column in the current table that will
                serve as the foreign key. This column must already have been
                added via :meth:`add_column`.
            refrences_table (Table): The referenced (parent) table object.
                This is typically an existing table instance from the driver.
            refrences_column (Column): The referenced column object in the
                parent table. This column should be the primary key or have a
                unique constraint.
            on_delete (ON_ACTION, optional): The action to take when a referenced
                row is deleted. Must be one of ``'CASCADE'``, ``'SET NULL'``,
                ``'SET DEFAULT'``, ``'RESTRICT'``, or ``'NO ACTION'``.
                Defaults to ``None`` (no ON DELETE clause).
            on_update (ON_ACTION, optional): The action to take when a referenced
                column value is updated. Same allowed values as ``on_delete``.
                Defaults to ``None`` (no ON UPDATE clause).

        Returns:
            TableStructure: The current instance, allowing method chaining.

        Raises:
            None: This method does not perform validation at call time. However,
                an invalid column name or reference may cause the final SQL
                statement to fail when executed by :meth:`Driver.create_table`
                or :meth:`Table.get_structure`.

        Example:
            Building a table with a foreign key reference to a ``users`` table::

                from ormophine.Mysql import TableStructure, DataTypes, Table

                # Assume `db` is a Driver instance with a `users` table
                users_table = db.users  # Table object

                orders = (TableStructure('orders')
                        .add_column('id', DataTypes.INT(), primary_key=True,
                                    auto_increment=True, not_null=True)
                        .add_column('user_id', DataTypes.INT(), not_null=True)
                        .add_column('product', DataTypes.VARCHAR(100))
                        .foreign_key('user_id', users_table, users_table.id,
                                    on_delete='CASCADE', on_update='CASCADE'))

                db.create_table(orders)
                # The generated table will include:
                # FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
                # ON DELETE CASCADE ON UPDATE CASCADE
        """
        self.foreigns.append(f'FOREIGN KEY ({column}) REFERENCES {refrences_table.name_} ({refrences_column.first_name}){f' ON DELETE {on_delete}' if on_delete else ''}{f' ON UPDATE {on_update}' if on_update else ''}')
        return self

    def get_structure(self):
        """
        Generate the complete CREATE TABLE SQL statement for the current table structure.

        This method assembles the internal column definitions, primary key constraints,
        foreign key constraints, and table options (engine, charset, collation) into
        a valid MySQL ``CREATE TABLE`` statement. It validates that at least one
        column has been defined before generating the SQL.

        Returns:
            str: A fully-formed ``CREATE TABLE`` SQL statement that can be executed
            to create the table in the database.

        Raises:
            Exception: If no columns have been added to the structure (i.e.,
                :meth:`get_columns` returns an empty list). The error message will
                indicate that at least one column is required.

        Example:
            Building a table structure and obtaining its SQL statement::

                from ormophine.Mysql import TableStructure, DataTypes

                struct = (TableStructure('users')
                        .add_column('id', DataTypes.INT(), primary_key=True,
                                    auto_increment=True, not_null=True)
                        .add_column('name', DataTypes.VARCHAR(100), not_null=True)
                        .add_column('email', DataTypes.VARCHAR(255), unique=True)
                        .foreign_key('email', 'profiles', 'user_email',
                                    on_delete='CASCADE'))

                sql = struct.get_structure()
                print(sql)
                # Output:
                # CREATE TABLE `users` (`id` INT NOT NULL AUTO_INCREMENT,
                # `name` VARCHAR(100) NOT NULL, `email` VARCHAR(255) UNIQUE,
                # PRIMARY KEY (`id`),
                # FOREIGN KEY (`email`) REFERENCES `profiles` (`user_email`) ON DELETE CASCADE)
                # ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
        """
        if self.get_columns():
            return f'CREATE TABLE {self.name} ({self.table_query[:-1]}{f', PRIMARY KEY({', '.join(self.primary_keys)})' if self.primary_keys else ''}{f', {','.join(self.foreigns)}' if self.foreigns else ''})  ENGINE=InnoDB DEFAULT CHARSET={self.charset} COLLATE={self.collate};' 
        else :
            raise Exception('You must add at least one column to create a table')

