diff --git a/backend/app/core/step_sync.py b/backend/app/core/step_sync.py index 5b92e5ca..cd90325f 100644 --- a/backend/app/core/step_sync.py +++ b/backend/app/core/step_sync.py @@ -170,15 +170,20 @@ async def sync_steps_from_tree( # Soft-delete previously synced steps that no longer exist in the tree current_node_ids = [s["source_node_id"] for s in extracted] if current_node_ids: + # Build NOT IN using explicit named placeholders — asyncpg does not + # auto-cast a Python list to a PostgreSQL array in text() queries. + placeholders = ", ".join(f":id_{i}" for i in range(len(current_node_ids))) + params = {f"id_{i}": nid for i, nid in enumerate(current_node_ids)} + params.update({"tree_id": str(tree_id), "now": now}) await db.execute( - text(""" + text(f""" UPDATE step_library SET is_active = false, updated_at = :now WHERE source_tree_id = :tree_id AND is_flow_synced = true - AND source_node_id != ALL(:node_ids) + AND source_node_id NOT IN ({placeholders}) """), - {"tree_id": str(tree_id), "node_ids": current_node_ids, "now": now} + params ) else: await db.execute( @@ -194,7 +199,11 @@ async def sync_steps_from_tree( async def deactivate_synced_steps_for_tree(db: AsyncSession, tree_id: UUID) -> None: - """Soft-delete all synced library entries for a tree (on tree delete/deactivate).""" + """Soft-delete all synced library entries for a tree (on tree delete/deactivate). + + Must be called BEFORE deleting the tree row — after deletion the FK ondelete='SET NULL' + will null source_tree_id, making the WHERE clause match nothing. + """ await db.execute( text(""" UPDATE step_library