"""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."""


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()
    """
    PLACE_HOLDER = '_MY_S4ULT3D_PL4C3_H0LD3R_%s_'
    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',
        innodb_flush_log_at_trx_commit: INNODB_FLUSH_LOG = 1
    ):
        """
        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'``.
            innodb_flush_log_at_trx_commit (INNODB_FLUSH_LOG, optional): InnoDB
                flush log setting (0, 1, or 2). Defaults to 1.

        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.PLACE_HOLDER = '_MY_S4ULT3D_PL4C3_H0LD3R_%s_'
        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}; SET SESSION innodb_flush_log_at_trx_commit = {innodb_flush_log_at_trx_commit};'
        }
        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))
        """
        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.
        """
        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 as e2:
                    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}')

    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.
        """
        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}')
    
    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)
                )
        """
        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}')

    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;")
        """
        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}')

    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)
        """
        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}')

    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)]
                )
        """
        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}')

    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
        """
        try:
            con.close()
        except:
            pass
        # حذف از storage اگر وجود دارد
        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='%')
        """
        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...
        """
        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()

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

    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.
        """
        self._output = (f'({self._output[0]} {'||' if self.col_obj.datatype == str else '+'} {other._output[0]})', self._output[1] + other._output[1]) if isinstance(other, ColumnsOperation) else (f'({self._output[0]} {'||' if self.col_obj.datatype == str else '+'} {other.name})', self._output[1]) if isinstance(other, Column) else (f'({self._output[0]} + %s)', self._output[1]+[other]) if isinstance(other, int) or isinstance(other , float) else (f'({self._output[0]} || %s)', self._output[1]+[other if isinstance(other, str) else str(other)])
        return self

    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)
        """
        self._output = (f'({other._output[0]} {'||' if self.col_obj.datatype == str else '+'} {self._output[0]})', other._output[1]+self._output[1]) if isinstance(other, ColumnsOperation) else (f'({other.name} {'||' if self.col_obj.datatype == str else '+'} {self._output[0]})', self._output[1]) if isinstance(other, Column) else (f'(%s + {self._output[0]})', [other]+self._output[1]) if isinstance(other, int) or isinstance(other , float) else (f'(%s || {self._output[0]})', [other if isinstance(other, str) else str(other)]+self._output[1])
        return self

    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: []
        """
        self._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 self

    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.
        """
        self._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 self

    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)', [])
        """
        self._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 self

    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)
        """
        self._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 self

    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)
        """
        self._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 self

    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)
        """
        self._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 self

    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)
        """
        self._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 self

    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)
        """
        self._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 self

    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))
        """
        self._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 self

    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)
        """
        self._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 self

    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)
        """
        if self._output:
            if key.start == None and key.stop ==  None:
                self._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:
                self._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:
                 self._output = (f'SUBSTRING({self._output[0]} , 1 , %s)', self._output[1] + [key.stop])  #  
            elif key.start >= 0 and key.stop ==  None:
                self._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:
                self._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:
                self._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:
                self._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:
                self._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:
                self._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:
                self._output = (f'SUBSTRING({self.col_obj.name} , 1 , LENGTH({self.col_obj.name}) + 1)', [])   #
            elif key.start == None and key.stop < 0:
                self._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:
                 self._output = (f'SUBSTRING({self.col_obj.name} , 1 , %s)', [key.stop])  #  
            elif key.start >= 0 and key.stop ==  None:
                self._output = (f'SUBSTRING({self.col_obj.name} , %s , LENGTH({self.col_obj.name}))', [key.start + 1])  #   
            elif key.start < 0 and key.stop == None:
                self._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:
                self._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:
                self._output = (f'SUBSTRING({self.col_obj.name} , %s , %s)', [key.start + 1, key.stop - key.start])  #
            elif key.start < 0 and key.stop < 0:
                self._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:
                self._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 self

    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.

        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)'
        """
        self._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 self

    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.

        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)'
        """
        self._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 self

    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.

        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)'
        """
        self._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 self

    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.

        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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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))
        """
        self._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 self

    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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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)'
        """
        self._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 self

    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)
        """
        self._output = (f'({self._output[0]} AND {value._output[0]})', self._output[1] + value._output[1])
        return self

    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)
        """
        self._output = (f'({self._output[0]} OR {value._output[0]})', self._output[1] + value._output[1])
        return self

    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.
        """
        self._output = (f"{self._output[0]} like {value._output[0]}", (self._output[1] + value._output[1]) if self._output 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 self

    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 || '%%')'
        """
        self._output = (f"{self._output[0]} like {prefix._output[0]} || '%%'", (self._output[1] + prefix._output[1]) if self._output 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 self

    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]
        """
        self._output = (f"{self._output[0]} like '%%' || {suffix._output[0]}", (self._output[1] + suffix._output[1]) if self._output 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 self

    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 || '%%')'
        """
        self._output = (f"{self._output[0]} like '%%' || {value._output[0]} || '%%'", (self._output[1] + value._output[1]) if self._output 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 self

    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)'
        """
        self._output = (f'({self._output[0]} || {content._output[0]})', self._output[1]+content._output[1] if self._output else content._output[1]) if isinstance(content, ColumnsOperation) else (f'({self._output[0]} || {content.name})', self._output[1] if self._output else []) if isinstance(content, Column) else (f'({self._output[0]} || %s)', self._output[1]+[content] if self._output else [content])
        return self

    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)'
        """
        self._output = (f'({content._output[0]} || {self._output[0]})', content._output[1]+self._output[1] if self._output else content._output[1]) if isinstance(content, ColumnsOperation) else (f'({content.name} || {self._output[0]})', self._output[1] if self._output else []) if isinstance(content, Column) else (f'(%s || {self._output[0]})', [content]+self._output[1] if self._output else [content])
        return self

    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']
        """
        self._output = (f'REPLACE({self._output[0]} , %s , %s)', self._output[1] + [old, new]) if self._output else (f'REPLACE({self.col_obj.name} , %s , %s)', [old, new])
        return self

    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']
        """
        self._output = (f'UPPER({self._output[0]})', self._output[1]) if self._output else (f'UPPER({self.col_obj.name})', [])
        return self

    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] -> [' ']
        """
        self._output = (f'LOWER({self._output[0]})', self._output[1]) if self._output else (f'LOWER({self.col_obj.name})', [])
        return self

    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))"
        """
        self._output = (f"TRIM(BOTH '{chars}' FROM {self._output[0]})", self._output[1]) if self._output else (f"TRIM(BOTH '{chars}' FROM {self.col_obj.name})", [])
        return self

    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))"
        """
        self._output = (f"TRIM(LEADING '{chars}' FROM {self._output[0]})", self._output[1]) if self._output else (f"TRIM(LEADING '{chars}' FROM {self.col_obj.name})", [])
        return self

    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))"
        """
        self._output = (f"TRIM(TRAILING '{chars}' FROM {self._output[0]})", self._output[1]) if self._output else (f"TRIM(TRAILING '{chars}' FROM {self.col_obj.name})", [])
        return self

    def In(self, value):
        """
        Generate an SQL ``IN`` condition or fall back to equality.

        This method creates a SQL fragment that checks whether the current
        expression is contained within a set of values or a subquery. The
        behavior depends on the type of ``value``:

        * If ``value`` is a :class:`ColumnsOperation`, it is treated as a
        subquery (or a set expression), and the SQL fragment becomes
        ``<expression> IN (<subquery>)``.
        * If ``value`` is a ``list`` or ``tuple``, an ``IN`` clause with
        placeholders is generated: ``<expression> IN (%s, %s, ...)``, and
        all items are added as parameters.
        * For any other single value, the method falls back to an equality
        condition: ``<expression> = %s``.

        The method updates the internal ``_output`` tuple (SQL fragment and
        parameter list) and returns ``self`` to enable method chaining.

        Args:
            value (ColumnsOperation, list, tuple, Any): The right-hand side of
                the condition. If a :class:`ColumnsOperation`, it is used as a
                subquery. If a list or tuple, it provides the set of values for
                the ``IN`` clause. Otherwise, the method generates an equality
                condition.

        Returns:
            ColumnsOperation: The same instance, with its ``_output`` attribute
            updated to reflect the ``IN`` or equality condition.

        Example:
            Using the ``In`` method to filter rows based on a list of values::

                from ormophine.Mysql import Table, Column

                # Assume users is a Table instance with a column 'id'
                condition = users.id.In([1, 2, 3])
                # condition._output[0] -> 'users.id IN (%s,%s,%s)'
                # condition._output[1] -> [1, 2, 3]

                # Using a subquery (e.g., select IDs from another table)
                subquery = other_table.id.gt(10)  # this would be a ColumnsOperation
                condition = users.id.In(subquery)
                # condition._output[0] -> 'users.id IN (other_table.id > %s)'
                # condition._output[1] -> [10]
        """
        self._output = (f"{self._output[0]} IN ({value._output[0]})",self._output[1] + value._output[1]) if isinstance(value, ColumnsOperation) else (f"{self._output[0]} IN ({','.join(['%s'] * len(value))})",self._output[1] + list(value)) if isinstance(value, (list, tuple)) else (f"{self._output[0]} = %s",self._output[1] + [value])
        return self

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 temp_ob ** value

    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.

        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} = %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.

        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} = %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.

        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} != %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.

        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} != %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 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 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 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 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 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 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 else value._output[1]) if isinstance(value, ColumnsOperation) else (f'{self.name} like {value.name}', temp_ob._output[1] if temp_ob._output else []) if isinstance(value , Column) else (f'{self.name} like %s', (temp_ob._output[1] + [f'{value}']) if temp_ob._output 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 else value._output[1]) if isinstance(value, ColumnsOperation) else (f"{self.name} like {value.name} || '%%'", temp_ob._output[1] if temp_ob._output else []) if isinstance(value , Column) else (f"{self.name} like %s || '%%'", (temp_ob._output[1] + [f'{value}']) if temp_ob._output 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 else value._output[1]) if isinstance(value, ColumnsOperation) else (f"{self.name} like '%%' || {value.name}", temp_ob._output[1] if temp_ob._output else []) if isinstance(value , Column) else (f"{self.name} like '%%' || %s", (temp_ob._output[1] + [f'{value}']) if temp_ob._output 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 else value._output[1]) if isinstance(value, ColumnsOperation) else (f"{self.name} like '%%' || {value.name} || '%%'", temp_ob._output[1] if temp_ob._output else []) if isinstance(value , Column) else (f"{self.name} like '%%' || %s || '%%'", (temp_ob._output[1] + [f'{value}']) if temp_ob._output 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, value):
        """
        Generate an SQL ``IN`` condition or fall back to equality for a column.

        This method creates a :class:`ColumnsOperation` object that represents
        either an ``IN`` clause (when a list, tuple, or subquery is provided) or
        an equality comparison (when a single value is given). The behavior
        depends on the type of ``value``:

        * If ``value`` is a :class:`ColumnsOperation`, it is treated as a subquery
        or set expression, and the SQL fragment becomes
        ``<column> IN (<subquery>)``.
        * If ``value`` is a ``list`` or ``tuple``, an ``IN`` clause with
        placeholders is generated: ``<column> IN (%s, %s, ...)``, and all
        items are added as parameters.
        * For any other single value, the method falls back to an equality
        condition: ``<column> = %s``.

        The returned :class:`ColumnsOperation` contains the generated SQL
        fragment and the associated parameter list, ready for use in queries
        like :meth:`Table.get_row` or :meth:`Table.update`.

        Args:
            value (ColumnsOperation, list, tuple, Any): The right‑hand side of
                the condition. If a :class:`ColumnsOperation`, it is used as a
                subquery. If a list or tuple, it provides the set of values for
                the ``IN`` clause. Otherwise, the method generates an equality
                condition.

        Returns:
            ColumnsOperation: A :class:`ColumnsOperation` instance representing
            the ``IN`` or equality condition, with its internal ``_output``
            attribute updated accordingly.

        Example:
            Using the ``In`` method to filter rows based on a list of values::

                from ormophine.Mysql import Table

                # Assume `users` is a Table instance with an `id` column
                condition = users.id.In([1, 2, 3])
                # condition._output[0] -> 'users.id IN (%s,%s,%s)'
                # condition._output[1] -> [1, 2, 3]

                # Using a subquery (e.g., select IDs from another table)
                subquery = other_table.id.gt(10)  # this returns a ColumnsOperation
                condition = users.id.In(subquery)
                # condition._output[0] -> 'users.id IN (other_table.id > %s)'
                # condition._output[1] -> [10]
        """
        temp_ob = ColumnsOperation(self)
        temp_ob._output = (f"{self.name} IN ({value._output[0]})",value._output[1]) if isinstance(value, ColumnsOperation) else (f"{self.name} IN ({','.join(['%s'] * len(value))})",list(value)) if isinstance(value, (list, tuple)) else (f"{self.name} = %s",[value])
        return temp_ob

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
        """
        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()
        """
        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)

class Join:
    """
    Namespace for creating JOIN specifications to be used in :meth:`Table.join`.

    The :class:`Join` class serves as a container for three nested classes:
    :class:`Inner`, :class:`Left`, and :class:`Right`. Each of these classes
    represents a specific type of SQL JOIN and encapsulates the target table
    and the join condition. When instantiated, they produce an object with a
    ``_output`` attribute (a tuple containing the SQL fragment and its
    parameters) that is consumed by :meth:`Table.join`.

    **Nested Classes**
        - :class:`Join.Inner`: Creates an ``INNER JOIN`` clause.
        - :class:`Join.Left`: Creates a ``LEFT JOIN`` clause.
        - :class:`Join.Right`: Creates a ``RIGHT JOIN`` clause.

    Each nested class has the same constructor signature:
        ``__init__(table: Table, match_case_condition: ColumnsOperation)``

    Example:
        Joining two tables using an INNER JOIN::

            from ormophine.Mysql import Join

            # Assume we have table objects: users, orders
            # and column objects: users.id, orders.user_id

            inner_join = Join.Inner(
                orders,
                users.id == orders.user_id
            )

            results = users.join(
                columns=[users.id, users.name, orders.amount],
                joins_list=[inner_join],
                where=users.id > 100
            )

        Using a LEFT JOIN::

            left_join = Join.Left(
                orders,
                users.id == orders.user_id
            )

            results = users.join(
                columns=[users.id, users.name, orders.amount],
                joins_list=[left_join]
            )

        Using a RIGHT JOIN::

            right_join = Join.Right(
                orders,
                users.id == orders.user_id
            )

            results = users.join(
                columns=[users.id, users.name, orders.amount],
                joins_list=[right_join]
            )

    Note:
        The join objects are not meant to be used independently; they are
        designed to be passed as a list to the ``joins_list`` parameter of
        :meth:`Table.join`. The join condition must be a
        :class:`ColumnsOperation` expression, typically created using
        comparison operators (``==``, ``!=``, ``>``, etc.) on :class:`Column`
        objects.
    """    
    class Inner:
        def __init__(self, table: Table, match_case_condition: ColumnsOperation):
            """
            Initialize an INNER JOIN clause between the current table and another table.

            This constructor creates an object that represents an ``INNER JOIN`` SQL
            clause. It stores both the SQL string and the associated parameter values
            for the join condition. The resulting object is typically used in a list
            passed to :meth:`Table.join` to perform multi-table queries.

            Args:
                table (Table): The table to join with. This is the right-hand side
                    table in the join.
                match_case_condition (ColumnsOperation): A :class:`ColumnsOperation`
                    expression that defines the join condition (e.g., ``users.id == orders.user_id``).
                    This condition will be used in the ``ON`` clause of the join.

            Returns:
                None: This method does not return a value; it initializes the instance.

            Raises:
                None: This constructor does not perform any validation and does not
                    raise exceptions.

            Example:
                Creating an INNER JOIN between the ``users`` table and the ``orders``
                table::

                    from ormophine.Mysql import Join

                    # Assuming we have table objects: users, orders
                    inner_join = Join.Inner(
                        orders,
                        users.id == orders.user_id
                    )

                    # Then use it in a join query
                    results = users.join(
                        columns=[users.name, orders.amount],
                        joins_list=[inner_join]
                    )
            """
            self._output =  (f'INNER JOIN {table.name_} ON {match_case_condition._output[0]}', match_case_condition._output[1])
            
    class Left:
        def __init__(self, table: Table, match_case_condition: ColumnsOperation):
            """
            Initialize a LEFT JOIN clause for a SQL query.

            This constructor creates a representation of a LEFT JOIN between the
            current table and the specified ``table``, using the provided condition.
            The resulting object stores a tuple ``_output`` containing the SQL fragment
            (e.g., ``'LEFT JOIN table_name ON condition'``) and the associated parameter
            list for safe parameterized execution. This object is intended to be used
            in the :meth:`Table.join` method.

            Args:
                table (Table): The table to join with.
                match_case_condition (ColumnsOperation): A :class:`ColumnsOperation`
                    expression defining the join condition (e.g., ``users.id == orders.user_id``).

            Returns:
                None: The constructor initializes the instance and stores the SQL
                fragment and parameters in ``self._output``.

            Raises:
                None: This method does not raise any exceptions directly.

            Example:
                Creating a LEFT JOIN between the ``users`` and ``orders`` tables::

                    from ormophine.Mysql import Join

                    join_clause = Join.Left(
                        orders,
                        users.id == orders.user_id
                    )

                    # The join clause can then be passed to Table.join()
                    results = users.join(
                        columns=[users.name, orders.amount],
                        joins_list=[join_clause]
                    )
            """
            self._output =  (f'LEFT JOIN {table.name_} ON {match_case_condition._output[0]}', match_case_condition._output[1])

    class Right:
        def __init__(self, table: Table, match_case_condition: ColumnsOperation):
            """
            Initialize a RIGHT JOIN clause for a query.

            This constructor creates a RIGHT JOIN specification that can be used
            in a :meth:`Table.join` call. A RIGHT JOIN returns all rows from the
            right table (the table being joined) and the matching rows from the
            left table (the base table). If no match is found on the left side,
            columns from the left table will contain ``NULL``.

            Args:
                table (Table): The table to join on the right side. This is the
                    table from which all rows will be returned (the "right" table).
                match_case_condition (ColumnsOperation): A :class:`ColumnsOperation`
                    expression defining the join condition, typically an equality
                    comparison between columns of the base table and the joined table
                    (e.g., ``users.id == orders.user_id``).

            Returns:
                None: This constructor only initializes the join object. The resulting
                object is meant to be passed to :meth:`Table.join`.

            Raises:
                Exception: If the underlying SQL generation or execution fails
                    (indirectly, when the join object is used in a query).

            Example:
                Performing a RIGHT JOIN between the ``users`` table and an
                ``orders`` table::

                    from ormophine.Mysql import Join

                    # Assume we have table objects: users, orders
                    # and column objects: users.id, orders.user_id, orders.amount

                    right_join = Join.Right(
                        orders,
                        users.id == orders.user_id
                    )

                    results = users.join(
                        columns=[users.id, users.name, orders.amount],
                        joins_list=[right_join],
                        where=users.id > 100
                    )
                    # This will return all orders, even those without a matching user
                    # (user columns will be NULL for unmatched orders).
            """
            self._output = (f'RIGHT JOIN {table.name_} ON {match_case_condition._output[0]}', match_case_condition._output[1])

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.
    """
    PLACE_HOLDER = '_MY_S4ULT3D_PL4C3_H0LD3R_%s_'
    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 = '_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 str if row[2].lower().split('(')[0] in ('char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext', 'enum', 'set', 'json', 'date', 'time', 'datetime', 'timestamp') 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,  # Python type (int, str, float, bytes)
            '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
            )
        """
        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' = None):
        """
        Retrieve rows from the table with flexible column selection and filtering.

        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 (returned as a table attribute) or a
                :class:`ColumnsOperation` expression (e.g., from arithmetic
                or string operations).
            where (ColumnsOperation, optional): A :class:`ColumnsOperation`
                expression representing the ``WHERE`` clause. If not provided,
                all rows are returned.
            order_by (Column, optional): A :class:`Column` object to order the
                results by. If provided, the query includes an ``ORDER BY`` clause
                on that column.

        Returns:
            If ``len(which_columns) == 1``: a list of the single column values
            (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):
            Retrieve specific columns with a condition::

                table = db.users
                # Get names and ages of users older than 18
                result = table.get_row(
                    which_columns=[table.name, table.age],
                    where=table.age > 18
                )
                # result: [('Alice', 25), ('Bob', 30), ...]

                # Get only names, ordered by age
                names = table.get_row(
                    which_columns=[table.name],
                    where=table.age > 18,
                    order_by=table.age
                )
                # names: ['Alice', 'Bob', ...]

        Example (Complex using ColumnsOperation):
            Use arithmetic and string operations in column selection::

                from ormophine.Mysql import ColumnsOperation

                table = db.products
                # Select product name, price with 10% tax, and full description
                # (concat name and category with a dash, then uppercase)
                expr = (table.name.add_end(' - ').add_end(table.category).upper())
                result = table.get_row(
                    which_columns=[
                        table.name,
                        table.price * 1.10,          # arithmetic
                        expr                         # string concatenation + upper
                    ],
                    where=(table.stock > 0) & (table.price < 100)
                )
                # result: [('Widget', 55.0, 'WIDGET - GADGETS'), ...]
        """
        tl = []
        wc = []
        [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]
        return [row[0] for row in (self._excfp(f'SELECT {', '.join(wc)} FROM {self.name_} WHERE {where._output[0]} {f'ORDER BY {order_by.first_name}' if order_by else ''};', tl+where._output[1]) if where else self._excfp(f'SELECT {', '.join(wc)} FROM {self.name_} {f'ORDER BY {order_by.first_name}' if order_by else ''};',tl) if tl else self._excf(f'SELECT {', '.join(wc)} FROM {self.name_} {f'ORDER BY {order_by.first_name}' if order_by else ''};',))] if len(which_columns) == 1 else self._excfp(f'SELECT {', '.join(wc)} FROM {self.name_} WHERE {where._output[0]} {f'ORDER BY {order_by.first_name}' if order_by else ''};', tl+where._output[1]) if where else self._excfp(f'SELECT {', '.join(wc)} FROM {self.name_} {f'ORDER BY {order_by.first_name}' if order_by else ''};',tl) if tl else self._excf(f'SELECT {', '.join(wc)} FROM {self.name_} {f'ORDER BY {order_by.first_name}' if order_by else ''};',)
        
    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.
        """
        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 str if data_type.lower().split('(')[0] in ('char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext', 'enum', 'set', 'json', 'date', 'time', 'datetime', 'timestamp') 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]), '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:
        """
        Perform a bulk update operation using a list of parameter sets.

        This method constructs a single parameterized ``UPDATE`` query where each
        occurrence of the ``PLACE_HOLDER`` string (by default
        ``'_MY_S4ULT3D_PL4C3_H0LD3R_%s_'``) in the SET clause and WHERE condition
        is replaced with a positional placeholder (``%s``). The actual values for
        each row are taken from the ``data_list``, and the query is executed once
        per row using ``executemany``. This is efficient for updating many rows
        with different data.

        The placeholder string is a class attribute and can be customized by
        assigning a new value to ``Table.PLACE_HOLDER`` or the instance attribute.

        Args:
            update (dict[Column, Any]): A dictionary mapping :class:`Column`
                objects to the new values. Values can be:

                - A :class:`Column` object (to set a column to the value of
                another column).
                - A :class:`ColumnsOperation` object (to set a column to a
                computed expression).
                - A literal value (e.g., ``int``, ``str``). If the value is a
                literal, it will be replaced with a placeholder unless it is
                the placeholder string itself (used for dynamic substitution
                from ``data_list``).
            where (ColumnsOperation): A :class:`ColumnsOperation` object
                representing the ``WHERE`` condition. The condition can contain
                placeholders to be substituted from ``data_list``.
            data_list (list): A list of sequences (lists or tuples), where each
                sequence contains the values to substitute for each placeholder
                in the order they appear in the query (first from SET clause,
                then from WHERE clause). The number of items in each sequence
                must match the total number of placeholders.

        Returns:
            None

        Raises:
            Exception: If the number of placeholders in the query does not match
                the number of items in each row of ``data_list``, an exception
                is raised with a detailed message. Also re-raises any database
                errors that occur during execution.

        Example:
            Updating multiple rows with different values::

                # Assume db is a Driver instance and users table has columns id, name, age.
                # We want to update age for users where name matches a list of names.

                # Define the update: set age = value from data_list (placeholder)
                # where name = value from data_list (placeholder)
                users.bulk_update(
                    update={users.age: users.PLACE_HOLDER},
                    where=users.name == users.PLACE_HOLDER,
                    data_list=[
                        [30, 'Alice'],
                        [25, 'Bob'],
                        [28, 'Charlie']
                    ]
                )
                # This executes:
                # UPDATE users SET age = %s WHERE name = %s;
                # with each pair from data_list.
        """
        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}={self.PLACE_HOLDER}' if not isinstance(value , ColumnsOperation) else f'{key.first_name}={value._output[0].replace('%s', self.PLACE_HOLDER)}' for key , value in list(update.items()))} WHERE {where._output[0].replace('%s', self.PLACE_HOLDER)};'.split(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 == 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(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 "{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 join(
        self,
        columns: list['Column'],
        joins_list: list['Join.Inner | Join.Left | Join.Right'],
        where: 'ColumnsOperation' = None,
        order_by: 'Column' = None
    ) -> Any:
        """
        Perform a SELECT query with JOINs across multiple tables.

        This method constructs and executes a SQL query that joins this table
        with other tables specified in ``joins_list``. The selected columns are
        returned with automatically generated aliases in the format
        ``{table_name}_{column_name}`` to avoid name collisions when columns from
        different tables have the same name. The result is fetched using the
        underlying driver's fetch method and returned as a list of tuples.

        Args:
            columns (list[Column]): A list of :class:`Column` objects or
                :class:`ColumnsOperation` expressions to select. Each will be
                included in the SELECT clause. For ``ColumnsOperation`` objects,
                the SQL expression is used as-is.
            joins_list (list[Union[Join.Inner, Join.Left, Join.Right]]): A list
                of join objects (inner, left, or right) that define which tables
                to join and the join conditions. Each join object is constructed
                with a target table and a condition (a :class:`ColumnsOperation`
                expression).
            where (ColumnsOperation, optional): A :class:`ColumnsOperation`
                expression for the WHERE clause. If provided, only rows satisfying
                this condition are returned. Defaults to ``None``.
            order_by (Column, optional): A :class:`Column` object to order the
                results by. If provided, an ``ORDER BY`` clause is added.
                Defaults to ``None``.

        Returns:
            list of tuple: A list of rows, where each row is a tuple of values
            corresponding to the selected columns (in the order given). The
            column values are accessible by position.

        Raises:
            Exception: If the underlying SQL execution fails (e.g., syntax error,
                invalid table or column references). The original error and the
                full query are included in the exception message.

        Example:
            Performing a join between the ``users`` table and an ``orders`` table::

                from ormophine.Mysql import Join

                # Assume we have table objects: users, orders
                # and column objects: users.id, users.name, orders.amount, orders.user_id

                # Build join objects
                inner_join = Join.Inner(
                    orders,
                    users.id == orders.user_id
                )

                # Select columns from both tables
                results = users.join(
                    columns=[users.id, users.name, orders.amount],
                    joins_list=[inner_join],
                    where=users.id > 100,
                    order_by=users.name
                )

                for row in results:
                    # row[0] -> user.id, row[1] -> user.name, row[2] -> order.amount
                    print(f"User {row[1]} (ID: {row[0]}) has order amount {row[2]}")
        """
        tl = []
        [tl.extend(i._output[1]) if isinstance(i,ColumnsOperation) else None for i in columns]
        [tl.extend(i._output[1]) for i in joins_list]
        return self._excfp(f'SELECT {','.join(f'{i.name} AS {i.table_obj.name_[1:-1]}_{i.first_name[1:-1]}'if isinstance(i,Column)else f'{i._output[0][1:-1] if i._output[0].startswith("(") and i._output[0].endswith(")")else i._output[0]} AS {i.col_obj.table_obj.name_[1:-1]}_{i.col_obj.first_name[1:-1]}' for i in columns)} FROM {self.name_} {' '.join(i._output[0] for i in joins_list)} {f'WHERE {where._output[0]}'if where else ''} {f'ORDER BY {order_by.name}' if order_by else ''}', tl+where._output[1]) if where else self._excfp(f'SELECT {','.join(f'{i.name} AS {i.table_obj.name_[1:-1]}_{i.first_name[1:-1]}' if isinstance(i,Column) else f'{i._output[0][1:-1] if i._output[0].startswith("(") and i._output[0].endswith(")") else i._output[0] } AS {i.col_obj.table_obj.name_[1:-1]}_{i.col_obj.first_name[1:-1]}' for i in columns)} FROM {self.name_} {' '.join(i._output[0] for i in joins_list)} {f'ORDER BY {order_by.name}' if order_by else ''}', tl) if tl else self._excf(f'SELECT {','.join(f'{i.name} AS {i.table_obj.name_[1:-1]}_{i.first_name[1:-1]}' if isinstance(i,Column) else f'{i._output[0][1:-1] if i._output[0].startswith('(') and i._output[0].endswith(')') else i._output[0] } AS {i.col_obj.table_obj.name_[1:-1]}_{i.col_obj.first_name[1:-1]}' for i in columns)} FROM {self.name_} {' '.join(i._output[0] for i in joins_list)} {f'ORDER BY {order_by.name}' if order_by else ''}')
        # The above line is approximately 1000 characters, which is not standard, but it is written this way
        # to improve performance in the Driver class and to avoid checking whether the second item in the query
        # is an empty list for each input.

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))
        """
        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"
        
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 must be NOT NULL.
                - 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 not not_null:
                raise Exception("PRIMARY KEY columns must also be NOT NULL.")
            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",
            "SERIAL"
        )

        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.")

        if 'SERIAL' in datatype:
            if not primary_key or not auto_increment or not not_null:
                raise Exception("SERIAL implies PRIMARY KEY, AUTO_INCREMENT, and NOT NULL.")
            
    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()}`'
        primary_key, not_null, auto_increment = (True, True, True) if datatype == 'SERIAL' else (primary_key, not_null, auto_increment)
        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 {f"'{default_value}'" if type(default_value) == str else default_value}' if default_value else ''},'
        return self

    def delete_column(self, column_name: str):
        """
        Remove a column from the table structure definition.

        This method deletes the specified column from the internal column registry,
        updates the accumulated SQL CREATE TABLE query fragment, and returns the
        :class:`TableStructure` instance for method chaining. If the column does
        not exist, an exception is raised.

        Args:
            column_name (str): The name of the column to delete. Leading/trailing
                whitespace is stripped, and the column name is automatically
                quoted with backticks.

        Returns:
            TableStructure: The current instance, allowing further method chaining
            (e.g., adding more columns or generating the final SQL).

        Raises:
            Exception: If no column with the given name exists in the table structure.

        Example:
            Building a table structure and then removing a column::

                from ormophine.Mysql import TableStructure, DataTypes

                table = (TableStructure('users')
                        .add_column('id', DataTypes.INT(), primary_key=True)
                        .add_column('name', DataTypes.VARCHAR(50))
                        .add_column('email', DataTypes.VARCHAR(255)))

                # Remove the 'email' column
                table.delete_column('email')

                # The final CREATE TABLE statement will only include 'id' and 'name'
                print(table.get_structure())
        """
        column_name = f'`{column_name.strip()}`'
        query_list = self.table_query.split(',')
        self.items.pop(column_name)
        for item in query_list:
            if item.strip().startswith(column_name):
                query_list.remove(item)
                self.table_query = ','.join(query_list)
                return self
        raise Exception(f'No column found with name ({column_name})')

    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')
