fix: delete ps node CASCADE
This commit is contained in:
@@ -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'])
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user