2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
14# CREATION DATE: 11-10-2025
15# LAST Modified: 14:52:25 19-12-2025
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
21# File in charge of containing the interfacing between an sql library and the program.
22# This contains functions that simplify the process of interracting with databases as well as check for injection attempts.
25# +==== END CatFeeder =================+
29from typing
import Optional, Union, List, Dict, Tuple, Literal, Any, overload
31from datetime
import datetime
33from display_tty
import Disp, initialise_logger
34from redis
import Redis
36 import mysql.connector
as mysql_connector
37 _MYSQL_ERROR_TUPLE: tuple = (mysql_connector.Error,)
39 mysql_connector =
None
40 _MYSQL_ERROR_TUPLE = tuple()
42from .sql_time_manipulation
import SQLTimeManipulation
43from .sql_connections
import SQLManageConnections
44from .sql_query_boilerplates
import SQLQueryBoilerplates
45from .sql_cache_orchestrator
import SQLCacheOrchestrator
46from .sql_redis_cache_rebinds
import RedisCaching, RedisArgs, SQLRedisCacheRebinds
47from ..core
import FinalClass
52_EXC_POOL_CLOSE = (AttributeError, RuntimeError) + _MYSQL_ERROR_TUPLE
55class SQL(metaclass=FinalClass):
56 """Manage database access and provide high-level query helpers.
58 This class wraps a low-level connection manager and exposes convenience
59 methods for common operations. Can auto-initialize via RuntimeManager or
60 use :py:meth:`create` for explicit factory initialization.
63 disp (Disp): Logger instance for debugging and error reporting.
64 sql_manage_connections (SQLManageConnections): Manages database connections.
65 sql_time_manipulation (SQLTimeManipulation): Handles time-related SQL operations.
66 sql_cache_orchestrator (SQLQueryBoilerplates): Provides query boilerplates.
67 debug (bool): Debug mode flag.
68 success (int): Numeric success code.
69 error (int): Numeric error code.
70 url (str): Database URL.
71 port (int): Database port.
72 username (str): Database username.
73 password (str): Database password.
74 db_name (str): Database name.
82 disp: Disp = initialise_logger(__qualname__,
False)
85 _runtime_error_string: str =
"SQLCacheOrchestrator method not initialized"
88 _wrapper_notice_begin: str =
"(Wrapper) Delegates to SQLQueryBoilerplates."
89 _wrapper_notice_end: str =
"\n\nOriginal docstring:\n"
92 _instace: Optional[
'SQL'] =
None
93 _initialization_attempted: bool =
False
94 _initialization_failed: bool =
False
100 def __init__(self, url: str, port: int, username: str, password: str, db_name: str, *, success: int = 0, error: int = 84, redis: Optional[Union[Redis, RedisArgs, RedisCaching]] =
None, redis_namespace: str =
"sql", redis_db_label: Optional[str] =
None, redis_default_ttls: Optional[Dict[str, int]] =
None, auto_initialize: bool =
True, debug: bool =
False):
101 """Initialize the SQL facade instance.
104 url (str): Database URL.
105 port (int): Database port.
106 username (str): Database username.
107 password (str): Database password.
108 db_name (str): Database name.
109 success (int, optional): Numeric success code. Defaults to 0.
110 error (int, optional): Numeric error code. Defaults to 84.
111 redis (union[Redis, RedisArgs, RedisCaching], optional): The arguments to initialise the redis caching instance. Defaults to None.
112 redis_namespace (str, optional): The name of the namespace to use in the redis environement. Defaults to sql.
113 redis_db_label (str, optional): Optional database label to isolate keys per DB. Defaults to the `DB_NAME` environment variable or "default". Defaults to None.
114 redis_default_ttls (Dict[str, str], optional): Optional TTLs in seconds per category (`version`, `schema`, `data`, `count`). Defaults to None.
115 auto_initialize (bool, optional): If True, automatically initialize connection pool. Defaults to True.
116 debug (bool, optional): Enable debug logging. Defaults to False.
119 self.
dispdisp.update_disp_debug(debug)
120 self.
dispdisp.log_debug(
"Initialising...")
161 if isinstance(redis, SQLRedisCacheRebinds):
163 elif isinstance(redis, RedisCaching):
165 existing_instance=redis,
168 elif isinstance(redis, (Redis, RedisArgs)):
171 namespace=redis_namespace,
172 db_label=redis_db_label,
173 default_ttls=redis_default_ttls
190 self.
dispdisp.log_debug(
"Initialised")
193 """Automatically initialize connection pool and query helpers.
195 Called during __init__ when auto_initialize=True.
196 Tracks initialization attempts to provide better error messages.
198 SQL._initialization_attempted =
True
202 SQL._initialization_failed =
True
203 msg =
"Failed to initialise the connection pool."
205 raise RuntimeError(f
"Error: {msg}")
222 except Exception
as e:
223 SQL._initialization_failed =
True
224 self.
dispdisp.log_critical(f
"Initialization failed: {e}")
228 """Best-effort cleanup invoked when the instance is garbage-collected.
230 This releases references to internal helpers so external resources
231 can be freed by the event loop later. Avoiding inside destructors.
247 """Ensure the SQL instance is initialized.
249 If initialization was never attempted, try to auto-initialize.
250 If initialization was attempted but failed, raise an error.
253 RuntimeError: If initialization has failed or cannot be performed.
258 if SQL._initialization_failed:
260 "SQL initialization has previously failed. Cannot perform operations on a failed instance."
263 if not SQL._initialization_attempted:
265 "Attempting lazy initialization",
"_ensure_initialized"
270 "SQL instance is not properly initialized. Use auto_initialize=True or call create() factory method."
277 def datetime_to_string(self, datetime_instance: datetime, date_only: bool =
False, sql_mode: bool =
False) -> str:
278 """(Wrapper) Delegates to SQLTimeManipulation.datetime_to_string
281 Runtime error if the class is not yet declared.
285 Format a :class:`datetime` to the project's string representation.
288 datetime_instance (datetime): Datetime to format.
289 date_only (bool): When True, return only the date portion.
290 sql_mode (bool): When True, include millisecond precision suitable
291 for insertion into SQL text fields.
294 ValueError: If ``datetime_instance`` is not a :class:`datetime`.
297 str: Formatted date/time string.
304 """(Wrapper) Delegates to SQLTimeManipulation.string_to_datetime
307 Runtime error if the class is not yet declared.
311 Parse a formatted date/time string into a :class:`datetime`.
314 datetime_string_instance (str): The string to parse.
315 date_only (bool): When True, parse using the date-only format.
318 ValueError: If the input is not a string or cannot be parsed.
321 datetime: Parsed :class:`datetime` instance.
328 """(Wrapper) Delegates to SQLManageConnection.is_connected
331 Runtime error if the class is not yet declared.
334 Check if there is an active connection to the database.
337 bool: The state of the connection.
344 """(Wrapper) Delegates to SQLTimeManipulation.get_correct_now_value
347 Runtime error if the class is not yet declared.
351 Return the current date/time formatted using the project's pattern.
354 str: Formatted current date/time string.
361 """(Wrapper) Delegates to SQLTimeManipulation.get_correct_current_date_value
364 Runtime error if the class is not yet declared.
368 Return the current date formatted using the project's date-only pattern.
371 str: Formatted current date string.
378 """(Wrapper) Delegates to SQLQueryBoilerplates.create_table
381 Runtime error if the class is not yet declared.
385 Fetch and parse the database version.
388 Optional[Tuple[Union[int, str], ...]]:
389 A tuple representing the database version, or None if the query fails.
397 """(Wrapper) Delegates to SQLQueryBoilerplates.create_table
400 Runtime error if the class is not yet declared.
404 Update the redis caching instance only with an initialised RedisCaching class.
405 This function has no effect if a non-initialised RedisCaching class or other arguments are passed.
408 redis_cacher (Optional[RedisCaching], optional): The initialised RedisCaching class instance. Defaults to None.
412 if isinstance(redis_cacher, RedisCaching)
and not callable(redis_cacher):
414 existing_instance=redis_cacher
419 """(Wrapper) Delegates to SQLQueryBoilerplates.get_table_column_names
422 Runtime error if the class is not yet declared.
426 Return the list of column names for ``table_name``.
429 table_name (str): Name of the table to inspect.
432 Union[List[str], int]: List of column names on success, or
433 ``self.error`` on failure.
440 """(Wrapper) Delegates to SQLQueryBoilerplates.get_table_names
443 Runtime error if the class is not yet declared.
447 Return a list of non-internal table names in the database.
450 Union[int, List[str]]: List of table names or ``self.error`` on failure.
457 """(Wrapper) Delegates to SQLQueryBoilerplates.get_triggers
460 Runtime error if the class is not yet declared.
464 Return a dictionary of all triggers and their SQL definitions.
467 Union[int, Dict[str, str]]: Dict of {trigger_name: sql_definition}, or ``self.error``.
474 """(Wrapper) Delegates to SQLQueryBoilerplates.get_trigger
477 Runtime error if the class is not yet declared.
481 Return a dictionary of all triggers and their SQL definitions.
484 Union[int, Dict[str, str]]: Dict of {trigger_name: sql_definition}, or ``self.error``.
491 """(Wrapper) Delegates to SQLQueryBoilerplates.get_trigger_names
494 Runtime error if the class is not yet declared.
498 Return a list of non-internal trigger names in the database.
501 Union[int, List[str]]: List of trigger names, or ``self.error`` on failure.
508 """(Wrapper) Delegates to SQLQueryBoilerplates.describe_table
511 Runtime error if the class is not yet declared.
515 Fetch the schema description for a table.
517 This returns rows similar to SQLite's PRAGMA table_info but is
518 transformed so the first element is the column name (to remain
519 compatible with previous MySQL-style DESCRIBE results).
522 table (str): Name of the table to describe.
525 RuntimeError: On critical SQLite errors (re-raised as RuntimeError).
528 Union[int, List[Any]]: Transformed description rows on success,
529 or ``self.error`` on failure.
535 def create_table(self, table: str, columns: List[Tuple[str, str]]) -> int:
536 """(Wrapper) Delegates to SQLQueryBoilerplates.create_table
539 Runtime error if the class is not yet declared.
543 Create a new table in the SQLite database.
546 table (str): Name of the new table.
547 columns (List[Tuple[str, str]]): List of (column_name, column_type) pairs.
550 int: ``self.success`` on success, or ``self.error`` on failure.
553 Example usage to create a basic ``users`` table:
555 .. code-block:: python
557 # Define the table name and column definitions
560 ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
561 ("username", "TEXT NOT NULL UNIQUE"),
562 ("email", "TEXT NOT NULL"),
563 ("created_at", "DATETIME DEFAULT CURRENT_TIMESTAMP")
566 # Create the table ronously
567 result = self.create_table(table_name, columns)
569 # Check if the operation succeeded
570 if result == self.success:
571 print(f"Table '{table_name}' created successfully.")
573 print(f"Failed to create table '{table_name}'.")
576 - This method automatically checks for SQL injection attempts using :class:`SQLInjection` before executing the query.
577 - Single quotes in table or column names are escaped defensively.
578 - The query uses ``CREATE TABLE IF NOT EXISTS`` to avoid errors if the table already exists.
584 def create_trigger(self, trigger_name: str, table_name: str, timing_event: str, body: str) -> int:
585 """(Wrapper) Delegates to SQLQueryBoilerplates.insert_trigger
588 Runtime error if the class is not yet declared.
592 Insert (create) a new SQL trigger into a MySQL or MariaDB database.
595 trigger_name (str): The name of the trigger to create.
596 table_name (str): The name of the table the trigger is being applied to.
597 timing_event (str): The rule when the event is to be triggered. e.g., 'BEFORE INSERT'.
598 body (str): The full SQL CREATE TRIGGER statement.
601 int: ``self.success`` on success, or ``self.error`` on failure.
607 def insert_trigger(self, trigger_name: str, table_name: str, timing_event: str, body: str) -> int:
608 """(Wrapper) Delegates to SQLQueryBoilerplates.insert_trigger
611 Runtime error if the class is not yet declared.
615 Insert (create) a new SQL trigger into a MySQL or MariaDB database.
618 trigger_name (str): The name of the trigger to create.
619 table_name (str): The name of the table the trigger is being applied to.
620 timing_event (str): The rule when the event is to be triggered. e.g., 'BEFORE INSERT'.
621 body (str): The full SQL CREATE TRIGGER statement.
624 int: ``self.success`` on success, or ``self.error`` on failure.
630 def insert_data_into_table(self, table: str, data: Union[List[List[Union[str,
None, int, float]]], List[Union[str,
None, int, float]]], column: Union[List[str],
None] =
None) -> int:
631 """(Wrapper) Delegates to SQLQueryBoilerplates.insert_data_into_table
634 Runtime error if the class is not yet declared.
638 Insert one or multiple rows into ``table``.
641 table (str): Table name.
642 data (Union[List[List[str]], List[str]]): Row data. Either a
643 single row (List[str]) or a list of rows (List[List[str]]).
644 column (List[str] | None): Optional list of columns to insert into.
647 int: ``self.success`` on success or ``self.error`` on failure.
657 column: Union[str, List[str]],
658 where: Union[str, List[str]] =
"",
659 beautify: Literal[
True] =
True,
660 ) -> Union[int, List[Dict[str, Any]]]: ...
666 column: Union[str, List[str]],
667 where: Union[str, List[str]] =
"",
668 beautify: Literal[
False] =
False,
669 ) -> Union[int, List[Tuple[Any, ...]]]: ...
671 def get_data_from_table(self, table: str, column: Union[str, List[str]], where: Union[str, List[str]] =
"", beautify: bool =
True) -> Union[int, Union[List[Dict[str, Any]], List[Tuple[Any, ...]]]]:
672 """(Wrapper) Delegates to SQLQueryBoilerplates.get_data_from_table
675 Runtime error if the class is not yet declared.
679 Query rows from ``table`` and optionally return them in a beautified form.
682 table (str): Table name.
683 column (Union[str, List[str]]): Column name(s) or '*' to select.
684 where (Union[str, List[str]], optional): WHERE clause or list of
685 conditions. Defaults to empty string.
686 beautify (bool, optional): If True, convert rows to list of dicts
687 keyed by column names. Defaults to True.
690 Union[int, List[Dict[str, Any]], List[Tuple[str, Any]]]: Beautified list of Dictionaries on success and if beautify is True, otherwise, a list of tuples is beautify is set to False, or ``self.error`` on failure.
696 def get_table_size(self, table: str, column: Union[str, List[str]], where: Union[str, List[str]] =
"") -> int:
697 """(Wrapper) Delegates to SQLQueryBoilerplates.get_table_size
700 Runtime error if the class is not yet declared.
704 Return the number of rows matching the optional WHERE clause.
707 table (str): Table name.
708 column (Union[str, List[str]]): Column to COUNT over (often '*').
709 where (Union[str, List[str]], optional): WHERE clause or list of
710 conditions. Defaults to empty string.
713 int: Number of matching rows, or ``SCONST.GET_TABLE_SIZE_ERROR`` on error.
719 def update_data_in_table(self, table: str, data: Union[List[List[Union[str,
None, int, float]]], List[Union[str,
None, int, float]]], column: List[str], where: Union[str, List[str]] =
"") -> int:
720 """(Wrapper) Delegates to SQLQueryBoilerplates.update_data_in_table
723 Runtime error if the class is not yet declared.
727 Update rows in ``table`` matching ``where`` with values from ``data``.
730 table (str): Table name.
731 data (List[str]): New values to set.
732 column (List): Column names corresponding to data.
733 where (Union[str, List[str]], optional): WHERE clause or list of
734 conditions. Defaults to empty string.
737 int: ``self.success`` on success, or ``self.error`` on failure.
743 def insert_or_update_data_into_table(self, table: str, data: Union[List[List[Union[str,
None, int, float]]], List[Union[str,
None, int, float]]], columns: Union[List[str],
None] =
None) -> int:
744 """(Wrapper) Delegates to SQLQueryBoilerplates.insert_or_update_data_into_table
747 Runtime error if the class is not yet declared.
751 Insert new rows or update existing rows for ``table``.
753 This method determines column names if not provided and delegates
754 to the appropriate INSERT/UPDATE boilerplate.
757 table (str): Table name.
758 data (Union[List[List[str]], List[str]]): Data to insert or update.
759 columns (List[str] | None, optional): Column names. Defaults to None.
762 int: ``self.success`` on success, or ``self.error`` on error.
769 """(Wrapper) Delegates to SQLQueryBoilerplates.insert_or_update_trigger
772 Runtime error if the class is not yet declared.
776 Insert (create) or update an SQL trigger into a MySQL or MariaDB database.
779 trigger_name (str): The name of the trigger to create.
780 table_name (str): The name of the table the trigger is being applied to.
781 timing_event (str): The rule when the event is to be triggered. e.g., 'BEFORE INSERT'.
782 body (str): The full SQL CREATE TRIGGER statement.
785 int: ``self.success`` on success, or ``self.error`` on failure.
792 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_data_from_table
795 Runtime error if the class is not yet declared.
799 Delete rows from ``table`` matching ``where``.
802 table (str): Table name to delete rows from.
803 where (Union[str, List[str]], optional): WHERE clause or list of
804 conditions to filter rows. If empty, all rows are deleted.
807 int: ``self.success`` on success or ``self.error`` on failure.
814 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_data_from_table
817 Runtime error if the class is not yet declared.
821 Delete rows from ``table`` matching ``where``.
824 table (str): Table name to delete rows from.
825 where (Union[str, List[str]], optional): WHERE clause or list of
826 conditions to filter rows. If empty, all rows are deleted.
829 int: ``self.success`` on success or ``self.error`` on failure.
837 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_table
840 Runtime error if the class is not yet declared.
844 Drop/Remove (delete) a table from the SQLite database.
847 table (str): Name of the table to drop.
850 int: ``self.success`` on success, or ``self.error`` on failure.
853 Example usage to drop the ``users`` table:
855 .. code-block:: python
858 result = self.drop_table(table_name)
860 if result == self.success:
861 print(f"Table '{table_name}' dropped successfully.")
863 print(f"Failed to drop table '{table_name}'.")
866 - The method performs SQL injection detection on the table name.
867 - If the table does not exist, no error is raised (uses ``DROP TABLE IF EXISTS`` internally).
874 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_table
877 Runtime error if the class is not yet declared.
881 Drop/Remove (delete) a table from the SQLite database.
884 table (str): Name of the table to drop.
887 int: ``self.success`` on success, or ``self.error`` on failure.
890 Example usage to drop the ``users`` table:
892 .. code-block:: python
895 result = self.drop_table(table_name)
897 if result == self.success:
898 print(f"Table '{table_name}' dropped successfully.")
900 print(f"Failed to drop table '{table_name}'.")
903 - The method performs SQL injection detection on the table name.
904 - If the table does not exist, no error is raised (uses ``DROP TABLE IF EXISTS`` internally).
911 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_trigger
914 Runtime error if the class is not yet declared.
918 Drop/Remove an existing SQL trigger if it exists.
921 trigger_name (str): Name of the trigger to drop.
924 int: ``self.success`` on success, or ``self.error`` on error.
931 """(Wrapper) Delegates to SQLQueryBoilerplates.remove_trigger
934 Runtime error if the class is not yet declared.
938 Drop/Remove an existing SQL trigger if it exists.
941 trigger_name (str): Name of the trigger to drop.
944 int: ``self.success`` on success, or ``self.error`` on error.
954 def create(cls, url: str, port: int, username: str, password: str, db_name: str, success: int = 0, error: int = 84, debug: bool =
False) ->
'SQL':
955 """factory to create and initialise an SQL instance.
957 This factory completes ronous initialisation steps that the
958 synchronous constructor cannot perform (notably the connection
959 pool initialisation). After this call returns the instance is ready
960 for usage and convenience callables are bound on the
964 url (str): DB host or file path (for sqlite this is a filename).
965 port (int): DB port (unused for sqlite but retained for API
967 username (str): DB username (unused for sqlite).
968 password (str): DB password (unused for sqlite).
969 db_name (str): Database name or sqlite filename.
970 success (int, optional): numeric success code used across the
971 sql helpers. Defaults to 0.
972 error (int, optional): numeric error code used across the sql
973 helpers. Defaults to 84.
974 debug (bool, optional): enable debug logging. Defaults to False.
977 SQL: Initialized SQL instance ready for operations.
980 RuntimeError: If the connection pool cannot be initialised.
983 sql = SQL.create('db.sqlite', 0, '', '', 'db.sqlite')
984 sql.get_data_from_table('my_table')
995 auto_initialize=
False,
1004 msg =
"Failed to initialise the connection pool."
1006 raise RuntimeError(f
"Error: {msg}")
1025 """Cleanly close resources like the connection pool."""
1029 except _EXC_POOL_CLOSE
as e:
1032 f
"Error while closing connection pool: {e}"
1035 if hasattr(self,
"disp")
and self.
dispdisp and status != self.
success:
1037 "Destroying the connection pool returned an error status."
int create_table(self, str table, List[Tuple[str, str]] columns)
'SQL' create(cls, str url, int port, str username, str password, str db_name, int success=0, int error=84, bool debug=False)
int drop_data_from_table(self, str table)
Union[int, List[str]] get_table_names(self)
int update_data_in_table(self, str table, Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]] data, List[str] column, Union[str, List[str]] where="")
Optional[SQLCacheOrchestrator] sql_cache_orchestrator
Optional[Tuple[int, int, int]] get_database_version(self)
str _get_correct_now_value
Union[int, List[Dict[str, Any]]] get_data_from_table(self, str table, Union[str, List[str]] column, Union[str, List[str]] where="", Literal[True] beautify=True)
Optional[Dict[str, int]] redis_default_ttls
Union[int, List[Any]] describe_table(self, str table)
str _get_correct_current_date_value
str get_correct_current_date_value(self)
int remove_trigger(self, str trigger_name)
Union[int, str] get_trigger(self, str trigger_name)
int remove_table(self, str table)
Union[List[str], int] get_table_column_names(self, str table_name)
None _auto_initialize(self)
datetime string_to_datetime(self, str datetime_string_instance, bool date_only=False)
str datetime_to_string(self, datetime datetime_instance, bool date_only=False, bool sql_mode=False)
int insert_or_update_data_into_table(self, str table, Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]] data, Union[List[str], None] columns=None)
None update_redis_cacher(self, Optional[RedisCaching] redis_cacher=None)
int create_trigger(self, str trigger_name, str table_name, str timing_event, str body)
int get_table_size(self, str table, Union[str, List[str]] column, Union[str, List[str]] where="")
int remove_data_from_table(self, str table, Union[str, List[str]] where="")
Optional[SQLQueryBoilerplates] _sql_query_boilerplate
str get_correct_now_value(self)
Optional[SQLRedisCacheRebinds] redis_cacher
Union[int, Dict[str, str]] get_triggers(self)
str _runtime_error_string
Optional[str] redis_db_label
int insert_data_into_table(self, str table, Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]] data, Union[List[str], None] column=None)
None _ensure_initialized(self)
Union[int, List[str]] get_trigger_names(self)
__init__(self, str url, int port, str username, str password, str db_name, *, int success=0, int error=84, Optional[Union[Redis, RedisArgs, RedisCaching]] redis=None, str redis_namespace="sql", Optional[str] redis_db_label=None, Optional[Dict[str, int]] redis_default_ttls=None, bool auto_initialize=True, bool debug=False)
int insert_or_update_trigger(self, str trigger_name, str table_name, str timing_event, str body)
int drop_table(self, str table)
int insert_trigger(self, str trigger_name, str table_name, str timing_event, str body)
int drop_trigger(self, str trigger_name)