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 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user