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 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 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
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 = []
visited_nodes = set()
# тут надо будет поработать с зацикливанием на вышестояющую ноду, сейчас вышестоящая нода если она уже была учтена, не будет занесена в спиок на удаление.
async def find_children_with_depth(current_node_id: int, current_depth: int):
if current_node_id in visited_nodes:
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, 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]:
"""
Возвращает список 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 = [node.id for node, depth in all_child_nodes]
ordered_node_ids.append(node_id)
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:
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"]
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)
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)
async def delete_ps_nodes_sequentially_with_error_handling(
connection: AsyncConnection, node_ids: List[int]
) -> List[int]:
async def delete_ps_nodes_delete_handler(connection: AsyncConnection, node_ids: List[int]) -> List[int]:
"""
Поочередно удаляет узлы из базы данных.
Возвращает список успешно удаленных ID узлов.
Выбрасывает исключение при первой ошибке.
"""
successfully_deleted = []
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:
successfully_deleted.append(node_id)
else:
raise Exception(f"Failed to delete node {node_id}: {error_message}")
return successfully_deleted

View File

@@ -17,7 +17,7 @@ from api.db.logic.ps_node import (
get_ps_node_by_id,
check_node_connection,
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
@@ -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)
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:
raise create_server_error(
message="Failed to delete nodes",

View File

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