This commit is contained in:
dj
2025-10-10 07:04:08 +03:00
parent 9a48d6f60f
commit c9099dac69
26 changed files with 738 additions and 405 deletions
+494
View File
@@ -0,0 +1,494 @@
# - *- 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()