2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
13# FILE: sql_cache_orchestrator.py
14# CREATION DATE: 18-11-2025
15# LAST Modified: 14:51:59 19-12-2025
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: File that contains the class in charge of calling the redis cache if present and fallback to sql querying if absent.
22# +==== END CatFeeder =================+
26from typing
import List, Dict, Union, Any, Tuple, Optional, Literal, overload, Callable
30from display_tty
import Disp, initialise_logger
33from .sql_injection
import SQLInjection
34from .sql_constants
import GET_TABLE_SIZE_ERROR
35from .sql_query_boilerplates
import SQLQueryBoilerplates
36from .sql_redis_cache_rebinds
import SQLRedisCacheRebinds
37from .sql_sanitisation_functions
import SQLSanitiseFunctions
42 disp: Disp = initialise_logger(__qualname__,
False)
44 def __init__(self, sql_query_boilerplates: SQLQueryBoilerplates, redis_cacher: Optional[SQLRedisCacheRebinds] =
None, success: int = 0, error: int = 84, debug: bool =
False) ->
None:
46 self.
disp.update_disp_debug(debug)
47 self.
disp.log_debug(
"Initialising...")
67 self.
disp.log_debug(
"Initialised")
70 """Normalize a cell value for parameter binding.
72 Converts special tokens (e.g., 'now', 'current_date') and preserves numeric
73 types. Returns None for null-like inputs.
76 cell (object): The cell value to normalize.
79 Union[str, None, int, float]: Normalized cell value.
83 if isinstance(cell, (int, float)):
87 if sl
in (
"now",
"now()"):
89 if sl
in (
"current_date",
"current_date()"):
93 def _parse_and_validate_where(self, where: Union[str, List[str]]) -> Tuple[Union[str, List[str]], List[Union[str, int, float,
None]]]:
94 """Parse WHERE clause, validate column names, and extract values for parameterization.
97 1. Parses WHERE clause to extract column names and values
98 2. Validates column names for SQL injection (not values)
99 3. Returns the original WHERE clause and extracted values list
102 where (Union[str, List[str]]): WHERE clause content.
105 Tuple[Union[str, List[str]], List[Union[str, int, float, None]]]:
106 - Original WHERE clause (unchanged, will be parameterized later)
107 - List of extracted values (empty if validation fails or no values)
110 RuntimeError: If SQL injection detected in column names.
112 if where ==
"" or (isinstance(where, list)
and len(where) == 0):
116 if isinstance(where, str):
119 where_list = list(where)
121 column_names: List[str] = []
123 for clause
in where_list:
124 clause_str = str(clause).strip()
127 if "=" in clause_str:
128 parts = clause_str.split(
"=", maxsplit=1)
130 column_name = parts[0].strip()
131 column_names.append(column_name)
134 if column_names
and self.
sql_injection.check_if_injections_in_strings(column_names):
136 "SQL injection detected in WHERE column names.")
137 raise RuntimeError(
"SQL injection detected in WHERE clause")
143 """Update the redis caching instance only with an initialised SQLRedisCacheRebinds class.
144 This function has no effect if a non-initialised SQLRedisCacheRebinds class or other arguments are passed.
147 redis_cacher (Optional[SQLRedisCacheRebinds], optional): The initialised SQLRedisCacheRebinds class instance. Defaults to None.
149 if isinstance(redis_cacher, SQLRedisCacheRebinds)
and not callable(redis_cacher):
153 """Fetch and parse the database version.
156 Optional[Tuple[int, int, int]]: A tuple representing the database version,
157 or None if the query fails.
160 resp: Optional[Tuple[int, int, int]] =
None
162 self.
disp.log_debug(
"Cacher instance is defined, calling.")
168 "No cacher instance defined, calling sql boilerplate directly."
170 resp = sql_function()
174 """Return the list of column names for a given table.
177 table_name (str): Name of the table to retrieve column names from.
180 Union[List[str], int]: List of column names on success, or `self.error` on failure.
183 resp: Union[List[str], int] = self.
error
185 self.
disp.log_debug(
"Cacher instance is defined, calling.")
187 table_name=table_name,
188 fetcher=sql_function,
189 error_token=self.
error
193 "No cacher instance defined, calling sql boilerplate directly."
195 resp = sql_function(table_name)
199 """Retrieve the names of all tables in the database.
202 Union[int, List[str]]: List of table names on success, or `self.error` on failure.
205 resp: Union[int, List[str]] = self.
error
207 self.
disp.log_debug(
"Cacher instance is defined, calling.")
209 fetcher=sql_function,
210 error_token=self.
error
214 "No cacher instance defined, calling sql boilerplate directly."
216 resp = sql_function()
220 """Retrieve all triggers and their SQL definitions.
223 Union[int, Dict[str, str]]: Dictionary of {trigger_name: sql_definition},
224 or `self.error` on failure.
227 resp: Union[int, Dict[str, str]] = self.
error
229 self.
disp.log_debug(
"Cacher instance is defined, calling.")
231 fetcher=sql_function,
232 error_token=self.
error
236 "No cacher instance defined, calling sql boilerplate directly."
238 resp = sql_function()
241 def get_trigger(self, trigger_name: str, db_name: Optional[str] =
None) -> Union[int, str]:
242 """Retrieve the SQL definition of a specific trigger.
245 trigger_name (str): The trigger name to fetch.
246 db_name (Optional[str], optional): Database name. Defaults to None.
249 Union[int, str]: The SQL definition, or `self.error` on failure.
252 resp: Union[int, str] = self.
error
254 self.
disp.log_error(
"Trigger name cannot be empty.")
257 to_check: List[str] = [trigger_name]
259 to_check.append(db_name)
261 if self.
sql_injection.check_if_injections_in_strings(to_check):
262 self.
disp.log_error(
"SQL Injection detected in trigger name.")
265 self.
disp.log_debug(
"Cacher instance is defined, calling.")
267 trigger_name=trigger_name,
268 fetcher=sql_function,
269 error_token=self.
error
273 "No cacher instance defined, calling sql boilerplate directly."
275 resp = sql_function(trigger_name)
279 """Return a list of trigger names in the current or specified MySQL database.
282 db_name (Optional[str], optional):
283 Name of the database/schema to query.
284 Defaults to None, which uses the currently selected database.
287 Union[int, List[str]]: List of trigger names, or ``self.error`` on failure.
290 resp: Union[int, List[str]] = self.
error
292 if self.
sql_injection.check_if_injections_in_strings([db_name]):
294 "SQL Injection detected in database name."
298 self.
disp.log_debug(
"Cacher instance is defined, calling.")
300 fetcher=sql_function,
301 error_token=self.
error
305 "No cacher instance defined, calling sql boilerplate directly."
307 resp = sql_function(db_name)
311 """Fetch the headers (description) of a table from the database.
314 table (str): The name of the table to describe.
317 Union[int, List[Any]]: A list containing the description of the table, or self.error if an error occurs.
320 resp: Union[List[str], int] = self.
error
322 self.
disp.log_error(
"Injection detected.",
"sql")
325 self.
disp.log_debug(
"Cacher instance is defined, calling.")
328 fetcher=sql_function,
329 error_token=self.
error
333 "No cacher instance defined, calling sql boilerplate directly."
335 resp = sql_function(table)
338 def create_table(self, table: str, columns: List[Tuple[str, str]]) -> int:
339 """Create a new table in the MySQL database, compatible with MySQL 5.0+.
342 table (str): Name of the new table.
343 columns (List[Tuple[str, str]]): List of (column_name, column_type) pairs.
346 int: ``self.success`` on success, or ``self.error`` on failure.
349 .. code-block:: python
353 ("id", "INT AUTO_INCREMENT PRIMARY KEY"),
354 ("username", "VARCHAR(255) NOT NULL UNIQUE"),
355 ("email", "VARCHAR(255) NOT NULL"),
356 ("created_at", "DATETIME DEFAULT CURRENT_TIMESTAMP")
359 result = self.create_table(table_name, columns)
360 if result == self.success:
361 print(f"Table '{table_name}' created successfully.")
363 print(f"Failed to create table '{table_name}'.")
366 - Protects against SQL injection using :class:`SQLInjection`.
367 - Escapes table and column names with backticks for MySQL.
368 - Includes version-aware fallback for MySQL 5.0 compatibility.
370 sql_function: Callable[
371 [str, List[Tuple[str, str]]],
374 resp: int = self.
error
375 if self.
sql_injection.check_if_injections_in_strings([table]):
376 self.
disp.log_error(
"Injection detected in table name.")
379 self.
disp.log_debug(
"Cacher instance is defined, calling.")
387 "No cacher instance defined, calling sql boilerplate directly."
389 resp = sql_function(table, columns)
392 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:
393 """Insert data into a table.
396 table (str): Name of the table.
397 data (Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]]): Data to insert.
398 column (Union[List[str], None], optional): List of column names. Defaults to None.
401 int: ``self.success`` on success, or ``self.error`` on failure.
404 resp: int = self.
error
408 List[List[Union[str,
None, int, float]]],
409 List[Union[str,
None, int, float]]
411 if column
is not None:
412 check_data.extend(column)
413 if isinstance(data, List):
415 if isinstance(i, List):
419 if self.
sql_injection.check_if_injections_in_strings(check_data)
is True:
420 self.
disp.log_error(
"Injection detected.",
"sql")
423 self.
disp.log_debug(
"Cacher instance is defined, calling.")
432 "No cacher instance defined, calling sql boilerplate directly."
434 resp = sql_function(table, data, column)
437 def insert_trigger(self, trigger_name: str, table_name: str, timing_event: str, body: str) -> int:
438 """Insert (create) a new SQL trigger into a MySQL or MariaDB database.
441 trigger_name (str): The name of the trigger to create.
442 table_name (str): The name of the table the trigger is being applied to.
443 timing_event (str): The rule when the event is to be triggered. e.g., 'BEFORE INSERT'.
444 body (str): The full SQL CREATE TRIGGER statement.
447 int: ``self.success`` on success, or ``self.error`` on failure.
450 resp: int = self.
error
451 if not all([trigger_name, table_name, timing_event, body]):
452 self.
disp.log_error(
"All parameters must be provided.")
455 self.
disp.log_debug(
"Cacher instance is defined, calling.")
457 trigger_name=trigger_name,
458 table_name=table_name,
459 timing_event=timing_event,
465 "No cacher instance defined, calling sql boilerplate directly."
467 resp = sql_function(trigger_name, table_name, timing_event, body)
474 column: Union[str, List[str]],
475 where: Union[str, List[str]] =
"",
476 beautify: Literal[
True] =
True,
477 ) -> Union[int, List[Dict[str, Any]]]: ...
483 column: Union[str, List[str]],
484 where: Union[str, List[str]] =
"",
485 beautify: Literal[
False] =
False,
486 ) -> Union[int, List[Tuple[Any, ...]]]: ...
488 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, ...]]]]:
489 """Fetch rows from a table.
492 table (str): Name of the table to query.
493 column (Union[str, List[str]]): Column selector; a single column name or a list of column names.
494 where (Union[str, List[str]]): Optional WHERE clause content; string or list joined by ``AND``.
495 beautify (bool): When True, return a list of dict rows; when False, return a list of tuples.
498 Union[int, List[Dict[str, Any]], List[Tuple[Any, Any]]]: Query result on success, or ``self.error`` on failure.
504 List[Dict[str, Any]],
505 List[Tuple[Any, Any]]
509 if self.
sql_injection.check_if_injections_in_strings([table, column])
is True:
510 self.
disp.log_error(
"Injection detected.",
"sql")
517 self.
disp.log_error(
"Injection detected in WHERE clause.",
"sql")
520 self.
disp.log_debug(
"Cacher instance is defined, calling.")
524 where=validated_where,
526 fetcher=sql_function,
527 error_token=self.
error
531 "No cacher instance defined, calling sql boilerplate directly."
533 resp = sql_function(table, column, validated_where, beautify)
536 def get_table_size(self, table: str, column: Union[str, List[str]], where: Union[str, List[str]] =
"") -> int:
537 """Return the row count for a table.
540 table (str): Name of the table to count rows in.
541 column (Union[str, List[str]]): Column expression passed to ``COUNT(...)``.
542 where (Union[str, List[str]]): Optional WHERE clause content; string or list joined by ``AND``.
545 int: Number of rows on success, or ``GET_TABLE_SIZE_ERROR`` on failure.
548 resp: int = self.
error
550 if self.
sql_injection.check_if_injections_in_strings([table, column])
is True:
551 self.
disp.log_error(
"Injection detected.",
"sql")
552 return GET_TABLE_SIZE_ERROR
558 self.
disp.log_error(
"Injection detected in WHERE clause.",
"sql")
559 return GET_TABLE_SIZE_ERROR
561 self.
disp.log_debug(
"Cacher instance is defined, calling.")
565 where=validated_where,
570 "No cacher instance defined, calling sql boilerplate directly."
572 resp = sql_function(table, column, validated_where)
575 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:
576 """Update rows in a table.
579 table (str): Name of the table to update.
580 data (Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]]): Values aligned to ``column`` order; single row or list of rows.
581 column (List[str]): Column names to update.
582 where (Union[str, List[str]]): Optional WHERE clause content; string or list joined by ``AND``.
585 int: ``self.success`` on success, or ``self.error`` on failure.
588 resp: int = self.
error
593 check_items = [table]
594 if isinstance(column, list):
595 check_items.extend([str(c)
for c
in column])
597 check_items.append(str(column))
598 if self.
sql_injection.check_if_injections_in_strings(check_items):
599 self.
disp.log_error(
"Injection detected.",
"sql")
606 self.
disp.log_error(
"Injection detected in WHERE clause.",
"sql")
609 self.
disp.log_debug(
"Cacher instance is defined, calling.")
614 where=validated_where,
619 "No cacher instance defined, calling sql boilerplate directly."
621 resp = sql_function(table, data, column, validated_where)
624 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:
625 """Insert or update rows using the first column as key.
628 table (str): Table name.
629 data (Union[List[List[Union[str, None, int, float]]], List[Union[str, None, int, float]]]): Single row or list of rows to upsert.
630 columns (Union[List[str], None], optional): Column names for ``data``; when None, infer columns from the table.
633 int: ``self.success`` on success, or ``self.error`` on failure.
636 resp: int = self.
error
639 check_list.extend(columns)
640 if self.
sql_injection.check_if_injections_in_strings(check_list):
641 self.
disp.log_error(
"SQL Injection detected.",
"sql")
644 self.
disp.log_debug(
"Cacher instance is defined, calling.")
653 "No cacher instance defined, calling sql boilerplate directly."
655 resp = sql_function(table, data, columns)
659 """Insert (create) or update an SQL trigger into a MySQL or MariaDB database.
662 trigger_name (str): The name of the trigger to create.
663 table_name (str): The name of the table the trigger is being applied to.
664 timing_event (str): The rule when the event is to be triggered. e.g., 'BEFORE INSERT'.
665 body (str): The full SQL CREATE TRIGGER statement.
668 int: ``self.success`` on success, or ``self.error`` on failure.
671 resp: int = self.
error
672 if self.
sql_injection.check_if_injections_in_strings([trigger_name, table_name]):
673 self.
disp.log_error(
"SQL injection detected.")
676 if self.
sql_injection.check_if_symbol_and_logic_gate_injection(timing_event):
677 self.
disp.log_error(
"SQL injection detected")
680 self.
disp.log_debug(
"Cacher instance is defined, calling.")
682 trigger_name=trigger_name,
683 table_name=table_name,
684 timing_event=timing_event,
690 "No cacher instance defined, calling sql boilerplate directly."
692 resp = sql_function(trigger_name, table_name, timing_event, body)
696 """Delete rows from a table.
699 table (str): Table name.
700 where (Union[str, List[str]]): Optional WHERE clause to limit deletions.
703 int: ``self.success`` on success, or ``self.error`` on failure.
706 resp: int = self.
error
709 self.
disp.log_error(
"Injection detected.",
"sql")
716 self.
disp.log_error(
"Injection detected in WHERE clause.",
"sql")
719 self.
disp.log_debug(
"Cacher instance is defined, calling.")
722 where=validated_where,
727 "No cacher instance defined, calling sql boilerplate directly."
729 resp = sql_function(table, validated_where)
733 """Drop a table from the MySQL database.
736 table (str): Name of the table to drop.
739 int: ``self.success`` on success, or ``self.error`` on failure.
742 - Performs SQL injection detection on the table name.
743 - Uses ``DROP TABLE IF EXISTS`` to avoid errors when the table is missing.
746 resp: int = self.
error
747 if self.
sql_injection.check_if_injections_in_strings([table]):
748 self.
disp.log_error(
"Injection detected in table name.")
751 self.
disp.log_debug(
"Cacher instance is defined, calling.")
758 "No cacher instance defined, calling sql boilerplate directly."
760 resp = sql_function(table)
764 """Drop/Remove an existing SQL trigger if it exists.
767 trigger_name (str): Name of the trigger to drop.
770 int: ``self.success`` on success, or ``self.error`` on error.
773 resp: int = self.
error
775 self.
disp.log_error(
"Trigger name cannot be empty.")
777 if self.
sql_injection.check_if_injections_in_strings([trigger_name]):
778 self.
disp.log_error(
"SQL Injection detected in trigger name.")
781 self.
disp.log_debug(
"Cacher instance is defined, calling.")
783 trigger_name=trigger_name,
788 "No cacher instance defined, calling sql boilerplate directly."
790 resp = sql_function(trigger_name)
Union[int, str] get_trigger(self, str trigger_name, Optional[str] db_name=None)
int insert_or_update_trigger(self, str trigger_name, str table_name, str timing_event, str body)
int insert_trigger(self, str trigger_name, str table_name, str timing_event, str body)
Union[List[str], int] get_table_column_names(self, str table_name)
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)
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="")
int remove_table(self, str table)
None update_redis_cacher(self, Optional[SQLRedisCacheRebinds] redis_cacher=None)
Optional[SQLRedisCacheRebinds] _redis_cacher
Union[int, List[str]] get_table_names(self)
None __init__(self, SQLQueryBoilerplates sql_query_boilerplates, Optional[SQLRedisCacheRebinds] redis_cacher=None, int success=0, int error=84, bool debug=False)
Tuple[Union[str, List[str]], List[Union[str, int, float, None]]] _parse_and_validate_where(self, Union[str, List[str]] where)
SQLQueryBoilerplates _sql_query_boilerplate
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)
SQLInjection sql_injection
Union[str, None, int, float] _normalize_cell(self, object cell)
Union[int, Dict[str, str]] get_triggers(self)
int create_table(self, str table, List[Tuple[str, str]] columns)
Optional[Tuple[int, int, int]] get_database_version(self)
Union[int, List[str]] get_trigger_names(self, Optional[str] db_name=None)
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)
Union[int, List[Any]] describe_table(self, str table)
int remove_trigger(self, str trigger_name)
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="")
SQLSanitiseFunctions sanitize_functions