fix: delete ps node CASCADE

This commit is contained in:
TheNoxium
2025-10-28 14:30:27 +05:00
parent a060f46e0a
commit 42741f4d98
4 changed files with 70 additions and 30 deletions

View File

@@ -0,0 +1,52 @@
"""add_cascade_delete_to_node_link_foreign_keys
Revision ID: 80840e78631e
Revises: cc3b95f1f99d
Create Date: 2025-10-26 18:47:24.004327
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '80840e78631e'
down_revision: Union[str, None] = 'cc3b95f1f99d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Drop existing foreign key constraints
# Note: These constraint names are MySQL auto-generated names
# If they differ, check with: SHOW CREATE TABLE node_link;
op.drop_constraint('node_link_ibfk_2', 'node_link', type_='foreignkey') # next_node_id
op.drop_constraint('node_link_ibfk_3', 'node_link', type_='foreignkey') # node_id
# Add new foreign key constraints with CASCADE
op.create_foreign_key(
'fk_node_link_next_node_id_cascade',
'node_link', 'ps_node',
['next_node_id'], ['id'],
ondelete='CASCADE'
)
op.create_foreign_key(
'fk_node_link_node_id_cascade',
'node_link', 'ps_node',
['node_id'], ['id'],
ondelete='CASCADE'
)
def downgrade() -> None:
"""Downgrade schema."""
# Drop CASCADE foreign key constraints
op.drop_constraint('fk_node_link_next_node_id_cascade', 'node_link', type_='foreignkey')
op.drop_constraint('fk_node_link_node_id_cascade', 'node_link', type_='foreignkey')
# Restore original foreign key constraints without CASCADE
op.create_foreign_key('node_link_ibfk_2', 'node_link', 'ps_node', ['next_node_id'], ['id'])
op.create_foreign_key('node_link_ibfk_3', 'node_link', 'ps_node', ['node_id'], ['id'])

View File

@@ -2,7 +2,7 @@ from typing import Optional, List
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import insert, select, desc, and_, or_, delete, update from sqlalchemy import insert, select, desc, and_, delete, update
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from orm.tables.process import ps_node_table, node_link_table, process_schema_table from orm.tables.process import ps_node_table, node_link_table, process_schema_table
@@ -89,13 +89,15 @@ async def check_node_connection(connection: AsyncConnection, node_id: int, next_
return result.mappings().first() is not None return result.mappings().first() is not None
async def get_all_child_nodes_with_depth(connection: AsyncConnection, node_id: int) -> List[tuple[Ps_Node, int]]: async def get_nodes_for_deletion_ordered(connection: AsyncConnection, node_id: int) -> List[int]:
""" """
Рекурсивно находит ВСЕ дочерние узлы с их уровнем вложенности. Рекурсивно находит ВСЕ дочерние узлы и возвращает их ID в правильном порядке:
от самых глубоких к корневым.
""" """
all_child_nodes = [] all_child_nodes = []
visited_nodes = set() visited_nodes = set()
# тут надо будет поработать с зацикливанием на вышестояющую ноду, сейчас вышестоящая нода если она уже была учтена, не будет занесена в спиок на удаление.
async def find_children_with_depth(current_node_id: int, current_depth: int): async def find_children_with_depth(current_node_id: int, current_depth: int):
if current_node_id in visited_nodes: if current_node_id in visited_nodes:
return return
@@ -117,28 +119,19 @@ async def get_all_child_nodes_with_depth(connection: AsyncConnection, node_id: i
await find_children_with_depth(node.id, current_depth + 1) await find_children_with_depth(node.id, current_depth + 1)
await find_children_with_depth(node_id, 0) await find_children_with_depth(node_id, 0)
return all_child_nodes
all_child_nodes.sort(key=lambda x: x[1], reverse=True)
async def get_nodes_for_deletion_ordered(connection: AsyncConnection, node_id: int) -> List[int]: ordered_node_ids = [node.id for node, depth in all_child_nodes]
"""
Возвращает список ID узлов для удаления в правильном порядке:
от самых последних к первым.
"""
child_nodes_with_depth = await get_all_child_nodes_with_depth(connection, node_id)
child_nodes_with_depth.sort(key=lambda x: x[1], reverse=True)
ordered_node_ids = [node.id for node, depth in child_nodes_with_depth]
ordered_node_ids.append(node_id) ordered_node_ids.append(node_id)
return ordered_node_ids return ordered_node_ids
async def delete_ps_node_by_id_completely(connection: AsyncConnection, node_id: int) -> tuple[bool, str]: async def delete_ps_node_by_id_CASCADE(connection: AsyncConnection, node_id: int) -> tuple[bool, str]:
""" """
Полностью удаляет узел из базы данных по ID. Полностью удаляет узел из базы данных по ID - ON DELETE CASCADE.
""" """
try: try:
node_query = select(ps_node_table).where(ps_node_table.c.id == node_id) node_query = select(ps_node_table).where(ps_node_table.c.id == node_id)
@@ -150,12 +143,6 @@ async def delete_ps_node_by_id_completely(connection: AsyncConnection, node_id:
ps_id = node_data["ps_id"] ps_id = node_data["ps_id"]
await connection.execute(
delete(node_link_table).where(
or_(node_link_table.c.node_id == node_id, node_link_table.c.next_node_id == node_id)
)
)
await remove_node_from_process_schema_settings(connection, ps_id, node_id) await remove_node_from_process_schema_settings(connection, ps_id, node_id)
result = await connection.execute(delete(ps_node_table).where(ps_node_table.c.id == node_id)) result = await connection.execute(delete(ps_node_table).where(ps_node_table.c.id == node_id))
@@ -172,23 +159,19 @@ async def delete_ps_node_by_id_completely(connection: AsyncConnection, node_id:
return False, str(e) return False, str(e)
async def delete_ps_nodes_sequentially_with_error_handling( async def delete_ps_nodes_delete_handler(connection: AsyncConnection, node_ids: List[int]) -> List[int]:
connection: AsyncConnection, node_ids: List[int]
) -> List[int]:
""" """
Поочередно удаляет узлы из базы данных. Поочередно удаляет узлы из базы данных.
Возвращает список успешно удаленных ID узлов. Возвращает список успешно удаленных ID узлов.
Выбрасывает исключение при первой ошибке.
""" """
successfully_deleted = [] successfully_deleted = []
for node_id in node_ids: for node_id in node_ids:
success, error_message = await delete_ps_node_by_id_completely(connection, node_id) success, error_message = await delete_ps_node_by_id_CASCADE(connection, node_id)
if success: if success:
successfully_deleted.append(node_id) successfully_deleted.append(node_id)
else: else:
raise Exception(f"Failed to delete node {node_id}: {error_message}") raise Exception(f"Failed to delete node {node_id}: {error_message}")
return successfully_deleted return successfully_deleted

View File

@@ -17,7 +17,7 @@ from api.db.logic.ps_node import (
get_ps_node_by_id, get_ps_node_by_id,
check_node_connection, check_node_connection,
get_nodes_for_deletion_ordered, get_nodes_for_deletion_ordered,
delete_ps_nodes_sequentially_with_error_handling, delete_ps_nodes_delete_handler,
) )
from api.db.logic.node_link import get_last_link_name_by_node_id, create_node_link_schema from api.db.logic.node_link import get_last_link_name_by_node_id, create_node_link_schema
@@ -98,7 +98,7 @@ async def delete_ps_node_endpoint(
ordered_node_ids = await get_nodes_for_deletion_ordered(connection, ps_node_delete_data.next_node_id) ordered_node_ids = await get_nodes_for_deletion_ordered(connection, ps_node_delete_data.next_node_id)
try: try:
deleted_node_ids = await delete_ps_nodes_sequentially_with_error_handling(connection, ordered_node_ids) deleted_node_ids = await delete_ps_nodes_delete_handler(connection, ordered_node_ids)
except Exception as e: except Exception as e:
raise create_server_error( raise create_server_error(
message="Failed to delete nodes", message="Failed to delete nodes",

View File

@@ -22,6 +22,7 @@ class BaseError(BaseModel):
""" """
Базовая модель ошибки. Базовая модель ошибки.
""" """
error_type: ErrorType error_type: ErrorType
message: str message: str
details: Optional[Dict[str, Any]] = None details: Optional[Dict[str, Any]] = None
@@ -31,6 +32,7 @@ class ServerError(BaseError):
""" """
Критические серверные ошибки (БД, соединения и прочие неприятности). Критические серверные ошибки (БД, соединения и прочие неприятности).
""" """
error_type: ErrorType = ErrorType.SERVER error_type: ErrorType = ErrorType.SERVER
@@ -38,6 +40,7 @@ class AccessError(BaseError):
""" """
Ошибки доступа (несоответствие тенантности, ролям доступа). Ошибки доступа (несоответствие тенантности, ролям доступа).
""" """
error_type: ErrorType = ErrorType.ACCESS error_type: ErrorType = ErrorType.ACCESS
@@ -45,6 +48,7 @@ class OperationError(BaseError):
""" """
Ошибки операции (несоответствие прохождению верификации, ошибки в датасете). Ошибки операции (несоответствие прохождению верификации, ошибки в датасете).
""" """
error_type: ErrorType = ErrorType.OPERATION error_type: ErrorType = ErrorType.OPERATION
@@ -52,5 +56,6 @@ class ValidationError(BaseError):
""" """
Ошибки валидации (несоответствие первичной валидации). Ошибки валидации (несоответствие первичной валидации).
""" """
error_type: ErrorType = ErrorType.VALIDATION error_type: ErrorType = ErrorType.VALIDATION
field_errors: Optional[Dict[str, str]] = None field_errors: Optional[Dict[str, str]] = None