# - *- coding: utf- 8 - *- import sqlite3 from typing import Any, Generic, TypeVar, Optional from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefinedType from tgbot.data.config import PATH_DATABASE from tgbot.database.adb_migration import CONST_MIGRATE_TABLES, CONST_MIGRATE_COLUMNS from tgbot.utils.const_functions import ded # noinspection PyProtectedMember ################################################################################ ############################### SUPPORT FUNCTIONS ############################## # Converting the resulting list into a dictionary def dict_factory(cursor, row) -> dict: save_dict = {} for idx, col in enumerate(cursor.description): save_dict[col[0]] = row[idx] return save_dict # Formatting a query without arguments def update_format(sql, parameters: dict) -> tuple[str, list]: values = ", ".join([ f"{item} = ?" for item in parameters ]) sql += f" {values}" return sql, list(parameters.values()) # Formatting a query with arguments def update_format_where(sql, parameters: dict) -> tuple[str, list]: sql += " WHERE " sql += " AND ".join([ f"{item} = ?" for item in parameters ]) return sql, list(parameters.values()) ################################################################################ ################# BASIC CLASS FOR WORKING WITH DATABASE TABLES ################# ModelTranslator = TypeVar("ModelTranslator", bound="ModelBase") class Databasex(Generic[ModelTranslator]): def __init__(self): self.storage_name = "storage" self.column_one = False self.table_model = None # Delete entry def delete(self, **kwargs): with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory sql = f"DELETE FROM {self.storage_name}" sql, parameters = update_format_where(sql, kwargs) con.execute(sql, parameters) # Clear all entries def clear(self): with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory sql = f"DELETE FROM {self.storage_name}" con.execute(sql) # Get entry def get(self, **kwargs) -> Optional[ModelTranslator]: with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory sql = f"SELECT * FROM {self.storage_name}" sql, parameters = update_format_where(sql, kwargs) response = con.execute(sql, parameters).fetchone() if response is not None: response = self.table_model(**response) return response # Get entries def gets(self, **kwargs) -> list[Optional[ModelTranslator]]: with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory sql = f"SELECT * FROM {self.storage_name}" sql, parameters = update_format_where(sql, kwargs) response = con.execute(sql, parameters).fetchall() return [self.table_model(**cache_object) for cache_object in response] # Get all entries def get_all(self) -> list[Optional[ModelTranslator]]: with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory sql = f"SELECT * FROM {self.storage_name}" response = con.execute(sql).fetchall() return [self.table_model(**cache_object) for cache_object in response] # Edit entry def update(self, **kwargs): ... # Export column names, default data, and type hints def fields(self) -> dict[str, list[dict[str, Any]]]: for field_name, field_data in self.table_model.model_fields.items(): field_name: str field_data: FieldInfo column_data = [ { 'column': field_name, 'type': field_data.annotation, 'value': field_data.default, } for field_name, field_data in self.table_model.model_fields.items() ] return {'column_data': column_data, 'column_one': self.column_one} ################################################################################ ################################################################################ # Export all tables from the Database to a list def database_export_fields_db() -> list: with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory database_tables = con.execute("SELECT * FROM sqlite_master where type='table'").fetchall() database_tables_db = [table['name'] for table in database_tables] # If there are service tables, remove them from the list of available tables. if "sqlite_sequence" in database_tables_db: database_tables_db.remove("sqlite_sequence") if "sqlite_master" in database_tables_db: database_tables_db.remove("sqlite_master") return database_tables_db # Export all tables, columns, and typehints from structures def database_export_fields_structure() -> dict: # Import all table models import tgbot.database as import_modeles # Storing all structure files (db_X.py) in a single list all_tables = [ getattr(import_modeles, name) for name in dir(import_modeles) if ( isinstance(getattr(import_modeles, name), type) and (name.istitle() and name.endswith("x")) ) ] save_tables = {} # Storage of all sorted and converted tables/columns # Iterating through tables and columns for table in all_tables: table_fields = table().fields() column_data = table_fields['column_data'] column_one = table_fields['column_one'] cache_columns = [] # If the table name is not in the dictionary, add it. if table().storage_name not in save_tables: save_tables[table().storage_name] = { 'column_one': column_one, 'column_datas': [], } # Column sorting for table_field in column_data: column_name = table_field['column'] column_type = table_field['type'].__name__ column_value = table_field['value'] # Replacing python date types with SQL column_type = column_type.replace( "str", "TEXT", ).replace( "int", "INTEGER", ).replace( "float", "REAL", ) # If there is no default value, replace it with - None if isinstance(column_value, PydanticUndefinedType): column_value = None # Default values based on type hints if column_type == "TEXT": if "status" in column_name.lower(): column_value = "False" else: column_value = "None" elif column_type == "INTEGER": column_value = 0 elif column_type == "REAL": column_value = 0.0 else: column_value = table_field['value'] save_tables[table().storage_name]['column_datas'].append( { 'column': column_name, 'type': column_type, 'value': column_value, } ) return save_tables ################################################################################ ################################################################################ # Database migration def database_migration(): with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory # If there are values for tables or columns, perform migration. if len(CONST_MIGRATE_TABLES) > 0 or len(CONST_MIGRATE_COLUMNS) > 0: print(f"|| Migrating | Data was found") # Export existing tables in the Database database_tables_db = database_export_fields_db() # Renaming tables - iterating through tables for table_name_later, table_name_new in CONST_MIGRATE_TABLES.items(): if table_name_later in database_tables_db: print(f"|| Migrating | Table '{table_name_later}' renamed to '{table_name_new}' (✓)") con.execute(f"ALTER TABLE {table_name_later} RENAME TO {table_name_new}") else: print(f"|| Migrating | Table '{table_name_later}' for renamed not found (X)") # Renaming columns - iterating through tables with columns inside for table_name in CONST_MIGRATE_COLUMNS: if table_name in database_tables_db: # Unloading existing columns from the current table columns_export = [ column['name'] for column in con.execute(f"PRAGMA table_info({table_name})").fetchall() ] # Browsing columns to rename them for column_name_later, column_name_new in CONST_MIGRATE_COLUMNS[table_name].items(): if column_name_later in columns_export: print( f"|| Migrating | Column '{column_name_later}' renamed to '{column_name_new}' from table '{table_name}' (✓)" ) con.execute( f"ALTER TABLE {table_name} RENAME COLUMN {column_name_later} TO {column_name_new}" ) else: print( f"|| Migrating | Column '{column_name_later}' from table '{table_name}' not found (X)" ) else: print(f"|| Migrating | Table '{table_name}' for renamed column(s) not found (X)") else: print(f"|| Migrating | Data not found") print(f"||") # Checking the relevance of columns (adding new ones, deleting old ones) def database_check_columns(): with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory symbol_log = True # Exporting tables and columns from structures fields_structure = database_export_fields_structure() for number, field in enumerate(fields_structure.items()): table_name: str = field[0] # Table name table_data: dict = field[1] # Table data column_datas = table_data['column_datas'] # Column name, type, and value columns_cache_add = [] # Columns that will be added to the database columns_cache_edit_columns = [] # Columns whose date types will be changed in the database columns_cache_edit_data = None # Which table columns will change date types in the database? # Unloading existing columns from the current table column_datas_types_db = [column for column in con.execute(f"PRAGMA table_info({table_name})").fetchall()] # Unloading existing column names from the current table columns_export_db = [column['name'] for column in column_datas_types_db] # Iterating through columns from a structure for column_structure in column_datas: # If a column from the structure is present in the database table if column_structure['column'] in columns_export_db: columns_export_db.remove(column_structure['column']) # Date type of the current column in the Structure column_type_structure = column_structure['type'] # Date type of the current column in the database column_type_database = [ column for column in con.execute(f"PRAGMA table_info({table_name})").fetchall() if column_structure['column'] == column['name'] ] # If the date column type (DB) is in the list, extract it. if len(column_type_database) > 0: column_type_database = column_type_database[0]['type'] # If the date types differ and have not been saved before, add them to the dictionary. if column_type_structure != column_type_database: if columns_cache_edit_data is None: columns_cache_edit_data = column_datas if column_structure['column'] not in columns_cache_edit_columns: columns_cache_edit_columns.append( f"{column_structure['column']}: {column_type_database} -> {column_type_structure} " ) else: # If a column from the structure is not present in the database, save it for addition. columns_cache_add.append(column_structure) # The remaining columns that are present in the database table but are missing from the structure columns_cache_delete = columns_export_db # Adding new columns for column in columns_cache_add: print(f"|| Modification | Column '{column['column']}' added to table '{table_name}'") con.execute( f"ALTER TABLE {table_name} ADD COLUMN {column['column']} {column['type']} DEFAULT '{column['value']}'" ) # Deleting existing obsolete columns for column in columns_cache_delete: print(f"|| Modification | Column '{column}' deleted from table '{table_name}'") con.execute(f"ALTER TABLE {table_name} DROP COLUMN {column}") # Changing the date types of columns in a table sql_edit_columns_create_table = [] sql_edit_columns_insert_values = [] columns_cache_edit_columns = ", ".join(columns_cache_edit_columns) if columns_cache_edit_data is not None: for column in columns_cache_edit_data: if column['type'] == "TEXT": sql_edit_columns_create_table.append( f"{column['column']} {column['type']} DEFAULT '{column['value']}'" ) else: sql_edit_columns_create_table.append( f"{column['column']} {column['type']} DEFAULT {column['value']}" ) sql_edit_columns_insert_values.append(column['column']) sql_edit_columns_create_table = ", ".join(sql_edit_columns_create_table) sql_edit_columns_create_table = f"CREATE TABLE {table_name}_new ({sql_edit_columns_create_table})" sql_edit_columns_insert_values = ", ".join(sql_edit_columns_insert_values) sql_edit_columns_insert_values = f"INSERT INTO {table_name}_new ({sql_edit_columns_insert_values}) SELECT {sql_edit_columns_insert_values} FROM {table_name}" con.execute(sql_edit_columns_create_table) # Creating a new table con.execute(sql_edit_columns_insert_values) # Transferring data from old columns to new ones con.execute(f"DROP TABLE {table_name};") con.execute(f"ALTER TABLE {table_name}_new RENAME TO {table_name};") print( f"|| Modification | Columns '{columns_cache_edit_columns}' was edit data type for table '{table_name}'" ) # Creating/deleting tables, columns, and records def database_initialization(): print("======== DATABASE PROCESSING ========") database_migration() # Migration check with sqlite3.connect(PATH_DATABASE) as con: con.row_factory = dict_factory # List of exported tables and columns from structures database_fields = database_export_fields_structure() # List of exported tables from the structure database_tables_import = [table for table in database_fields.keys()] # List of tables available in the Database database_tables_db = database_export_fields_db() # List of tables to be deleted that are not present in imports but are present in the database database_tables_delete = [ table for table in database_tables_db if table not in database_tables_import ] # Browse the list of imported tables, columns, and default records to create them. for number, db_field in enumerate(database_fields.items()): table_name: str = db_field[0] # Table name table_data: dict = db_field[1] # Table data column_one = table_data['column_one'] # Will the table have one record or more column_datas = table_data['column_datas'] # Column name, data type, and value column_len = len(column_datas) # Length (number) of all columns in the table # Checking for the required number of records in a table if len(con.execute(f"PRAGMA table_info({table_name})").fetchall()) == column_len: print(f"|| Creating | Table '{table_name}' was found ({number + 1}/{len(database_fields)})") else: print(f"|| Creating | Table '{table_name}' not found ({number + 1}/{len(database_fields)})") # Column names with type hints (user_id INTEGER, user_name TEXT) columns_cache_create = [] # Column names without type hints (user_id, user_name) columns_cache_insert = [] # Default data for automatic entry into columns (settings, payment systems) columns_values = [] ############################################################ ##################### CREATING TABLES ###################### for column in column_datas: column_name = column['column'] # Table name column_value = column['value'] # Default value in column column_type = column['type'] # Entry data type (INTEGER, TEXT, REAL, ...) # Incrementable entry check if column_name == "increment": column_execute_create = "increment INTEGER PRIMARY KEY AUTOINCREMENT" else: column_execute_create = f"{column_name} {column_type}" columns_cache_insert.append(column_name) columns_cache_create.append(column_execute_create) # If default data is available, add it. if column_one and column_value is not None: columns_values.append(column_value) # Collecting a request with all added columns column_execute_create = ",\n".join(columns_cache_create) column_execute_create = ded(f""" CREATE TABLE IF NOT EXISTS {table_name}( {column_execute_create} ) """) # Request to create a table and columns con.execute(column_execute_create) ############################################################ ####################### ADDING ENTRY ####################### # If there is a record to add and the table supports only one record if column_one and len(columns_values) > 0: column_check_have = con.execute(f"SELECT * FROM {table_name}").fetchall() # If there is no entry in the table, add it. if len(column_check_have) == 0: # Request collection column_execute_insert = ",\n".join(columns_cache_insert) column_execute_insert = ded(f""" INSERT INTO {table_name}( {column_execute_insert} ) VALUES ({"?," * (len(columns_cache_insert) - 1) + "?"}) """) # Request to add entries to the table con.execute(column_execute_insert, columns_values) print(f"|| Creating | Table '{table_name}' was created ({number + 1}/{len(database_fields)})") # If there are tables to be deleted, delete them. if len(database_tables_delete) > 0: for table_name in database_tables_delete: print(f"|| Database | Table '{table_name}' was deleted") con.execute(f"DROP TABLE {table_name}") print("||") database_check_columns() # Checking the relevance of columns print("=====================================") print()