Skip to content

fix: prevent ForeignKeyViolation when deleting flows with traces#12499

Open
octo-patch wants to merge 1 commit intolangflow-ai:mainfrom
octo-patch:fix/issue-12346-flow-deletion-foreignkey-cascade
Open

fix: prevent ForeignKeyViolation when deleting flows with traces#12499
octo-patch wants to merge 1 commit intolangflow-ai:mainfrom
octo-patch:fix/issue-12346-flow-deletion-foreignkey-cascade

Conversation

@octo-patch
Copy link
Copy Markdown

@octo-patch octo-patch commented Apr 4, 2026

Fixes #12346

Problem

Deleting a flow that has associated execution traces raises a ForeignKeyViolation in PostgreSQL. The root cause is that the span.trace_id FK was created without ON DELETE CASCADE in migration 3478f0bd6ccb. When PostgreSQL cascades the flow → trace deletion, it then tries to delete trace rows, but span records still reference them — causing a constraint error.

There are two compounding issues:

  1. span.trace_id FK missing ON DELETE CASCADE at the database level
  2. cascade_delete_flow() never explicitly deletes TraceTable or SpanTable records, relying entirely on the broken DB-level cascade chain

Solution

Application-level (defence-in-depth): cascade_delete_flow() now explicitly deletes SpanTable rows (keyed by trace IDs for the flow) and TraceTable rows before deleting the Flow, matching the existing pattern for MessageTable, TransactionTable, VertexBuildTable, and FlowVersion.

Database-level: New Alembic migration a3f8b2c91d04 drops and re-creates the span.trace_id FK with ON DELETE CASCADE. Uses batch_alter_table for SQLite compatibility and introspects the actual FK constraint name (handles both auto-generated and named constraints).

ORM model: SpanTable.trace_id Field gains ondelete="CASCADE" so new deployments get the correct schema from the start.

Testing

  • Create a flow, execute it to generate traces/spans, then delete the flow — should succeed without ForeignKeyViolation
  • Run alembic upgrade head on both PostgreSQL and SQLite
  • Run alembic downgrade -1 to verify reversibility

Summary by CodeRabbit

  • Bug Fixes
    • Improved automatic cleanup of trace and span data when deleting flows, ensuring no orphaned records remain in the database.

Fixes langflow-ai#12346

When deleting a flow that has associated execution traces, PostgreSQL
raises a ForeignKeyViolation because the span.trace_id FK was created
without ON DELETE CASCADE, leaving spans dangling when traces are
deleted during the flow->trace cascade chain.

Two-pronged fix:
- cascade_delete_flow() now explicitly deletes SpanTable rows (grouped
  by trace) and TraceTable rows before deleting the Flow, matching the
  existing pattern used for Message/Transaction/VertexBuild tables.
- SpanTable.trace_id Field gains ondelete="CASCADE" so new deployments
  get the correct schema from the ORM.
- New Alembic migration (a3f8b2c91d04) drops and re-creates the
  span.trace_id FK with ON DELETE CASCADE for existing deployments.
@github-actions github-actions Bot added the community Pull Request from an external contributor label Apr 4, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

Walkthrough

This PR implements a two-pronged fix for flow deletion failures caused by missing cascade constraints on the span.trace_id foreign key. It adds an Alembic migration to modify the database constraint, updates the ORM model definition, and enhances the application-level deletion function to explicitly handle trace and span records before deleting the flow.

Changes

Cohort / File(s) Summary
Database Migration
src/backend/base/langflow/alembic/versions/a3f8b2c91d04_add_ondelete_cascade_to_span_trace_id_fk.py
New migration that modifies the span.trace_id foreign key constraint to include ON DELETE CASCADE. The upgrade() inspects and conditionally drops/recreates the constraint; downgrade() reverses the change.
ORM Model Definition
src/backend/base/langflow/services/database/models/traces/model.py
Updated SpanTable.trace_id field to include ondelete="CASCADE" in the foreign key constraint definition.
Cascade Deletion Logic
src/backend/base/langflow/api/utils/core.py
Enhanced cascade_delete_flow function to explicitly delete SpanTable and TraceTable records matching the given flow_id before proceeding with existing deletions of FlowVersion and Flow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings, 2 inconclusive)

Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error No test files added to verify cascade_delete_flow function handles ForeignKeyViolation when deleting flows with traces and spans. Add regression tests validating flows with traces/spans delete successfully without errors, and test the cascade delete logic and migration on PostgreSQL and SQLite.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage ⚠️ Warning PR implements cascade deletion fixes but lacks automated test coverage for the new functionality. Add unit tests for cascade_delete_flow(), integration tests for migration upgrade/downgrade, and ORM cascade semantics validation.
Test File Naming And Structure ❓ Inconclusive No test files were found to be included in this PR covering the cascade_delete_flow modifications, Alembic migration, or SpanTable changes. Clarify whether tests are required; if yes, create test files following pytest patterns covering cascade deletion, migration reversibility, and FK constraint behavior.
Excessive Mock Usage Warning ❓ Inconclusive Shell commands were provided for searching test files related to modified modules (core.py, cascade_delete_flow) and checking existing test patterns with mocks. The verification requires actual execution of the shell commands against a specific codebase to determine if test files exist and contain relevant mock patterns; the search queries alone cannot validate whether tests are present or properly structured.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main fix: preventing ForeignKeyViolation errors when deleting flows with traces.
Linked Issues check ✅ Passed All changes comprehensively address the linked issue #12346: application-level explicit deletions in cascade_delete_flow [#12346], database-level migration with ON DELETE CASCADE [#12346], and ORM model updates [#12346].
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving the ForeignKeyViolation issue; no unrelated modifications or functionality changes are present.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Apr 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/backend/base/langflow/alembic/versions/a3f8b2c91d04_add_ondelete_cascade_to_span_trace_id_fk.py`:
- Around line 23-29: The downgrade() currently returns early when
_get_fk_constraint_name(...) yields None (so SQLite leaves the CASCADE FK in
place); update downgrade() to handle fk_name is None by falling back to
inspecting inspector.get_foreign_keys(table_name) to find the FK entry by
constrained_columns and referred_table/columns and then recreate the FK without
ON DELETE CASCADE (i.e., drop and re-add the FK constraint or recreate the table
schema with the non-CASCADE FK) instead of returning; modify
_get_fk_constraint_name/downgrade to use the FK metadata (constrained_columns,
referred_table, referred_columns) to identify and rebuild the constraint when
the inspector cannot provide a name.

In `@src/backend/base/langflow/api/utils/core.py`:
- Around line 356-358: The current code materializes trace IDs by calling
session.exec(sm_select(TraceTable.id)...) and then uses Python-level trace_ids
in SpanTable.trace_id.in_(trace_ids), which will build a huge IN(...) list for
large flows; change to perform a SQL-level subquery delete so the DB deletes
spans directly without loading IDs into Python—replace the two-step pattern with
a single delete against SpanTable where SpanTable.trace_id is in a subquery
selecting TraceTable.id (use the existing
sm_select/TraceTable/SpanTable/delete/session.exec symbols to construct the
subquery so the database executes the deletion).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 71e4f149-bfbe-4305-a2f9-f7d7ae661c19

📥 Commits

Reviewing files that changed from the base of the PR and between e555e47 and 43937d5.

📒 Files selected for processing (3)
  • src/backend/base/langflow/alembic/versions/a3f8b2c91d04_add_ondelete_cascade_to_span_trace_id_fk.py
  • src/backend/base/langflow/api/utils/core.py
  • src/backend/base/langflow/services/database/models/traces/model.py

Comment on lines +23 to +29
def _get_fk_constraint_name(conn, table_name: str, column_name: str) -> str | None:
"""Find the foreign key constraint name for a given column."""
inspector = sa.inspect(conn)
for fk in inspector.get_foreign_keys(table_name):
if column_name in fk["constrained_columns"]:
return fk["name"]
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether SQLite exposes FK names via SQLAlchemy inspector in your env.
python - <<'PY'
import sqlalchemy as sa

engine = sa.create_engine("sqlite:///:memory:")
with engine.begin() as conn:
    conn.exec_driver_sql("PRAGMA foreign_keys=ON")
    conn.exec_driver_sql("CREATE TABLE trace (id TEXT PRIMARY KEY)")
    conn.exec_driver_sql(
        "CREATE TABLE span (id TEXT PRIMARY KEY, trace_id TEXT, "
        "FOREIGN KEY(trace_id) REFERENCES trace(id) ON DELETE CASCADE)"
    )

insp = sa.inspect(engine)
fks = insp.get_foreign_keys("span")
print("FK metadata:", fks)
print("FK names:", [fk.get("name") for fk in fks])
PY

Repository: langflow-ai/langflow

Length of output: 186


🏁 Script executed:

cd /tmp && find . -path "*/langflow/alembic/versions/a3f8b2c91d04*" -type f 2>/dev/null | head -5

Repository: langflow-ai/langflow

Length of output: 46


🏁 Script executed:

git ls-files | grep -E "(alembic|migration)" | head -20

Repository: langflow-ai/langflow

Length of output: 1430


🏁 Script executed:

# Look for the migration file mentioned in the review
git ls-files | grep "a3f8b2c91d04"

Repository: langflow-ai/langflow

Length of output: 163


🏁 Script executed:

# Check pyproject.toml or requirements.txt for dependencies
git ls-files | grep -E "(requirements|setup|pyproject|poetry)" | head -10

Repository: langflow-ai/langflow

Length of output: 433


🏁 Script executed:

# Find and examine the migration file
find . -name "*a3f8b2c91d04*" -type f 2>/dev/null

Repository: langflow-ai/langflow

Length of output: 165


🏁 Script executed:

cat -n src/backend/base/langflow/alembic/versions/a3f8b2c91d04_add_ondelete_cascade_to_span_trace_id_fk.py

Repository: langflow-ai/langflow

Length of output: 2605


🏁 Script executed:

# Check the pyproject.toml for SQLAlchemy version
cat pyproject.toml | grep -A 5 -B 5 "sqlalchemy"

Repository: langflow-ai/langflow

Length of output: 406


🏁 Script executed:

# Look for similar migration patterns in the codebase
rg "get_foreign_keys" --type py

Repository: langflow-ai/langflow

Length of output: 3143


🏁 Script executed:

# Check the migration utility module to understand how FK names are handled
cat src/backend/base/langflow/utils/migration.py | head -100

Repository: langflow-ai/langflow

Length of output: 2174


🏁 Script executed:

# Look at the previous similar migration to understand the pattern
cat -n src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py

Repository: langflow-ai/langflow

Length of output: 2580


🏁 Script executed:

# Check test files for migration validation and FK behavior
cat -n src/backend/tests/unit/alembic/test_migration_execution.py | head -150

Repository: langflow-ai/langflow

Length of output: 5611


🏁 Script executed:

# Look for any database configuration or SQLite-specific handling in migrations
rg "sqlite" --type py | head -20

Repository: langflow-ai/langflow

Length of output: 2249


🏁 Script executed:

# Check if there are any tests specifically for the downgrade behavior
find . -name "*test*migration*" -type f -exec grep -l "downgrade" {} \;

Repository: langflow-ai/langflow

Length of output: 247


🏁 Script executed:

# Look at how other migrations handle the FK name None case
rg "if fk_name is None" --type py -A 3

Repository: langflow-ai/langflow

Length of output: 3318


🏁 Script executed:

# Check the SQLAlchemy version being used
cat pyproject.toml | grep -A 2 "sqlalchemy"

Repository: langflow-ai/langflow

Length of output: 179


🏁 Script executed:

# Check test files for migration behavior testing
cat -n src/backend/tests/unit/alembic/test_existing_migrations.py

Repository: langflow-ai/langflow

Length of output: 4816


🏁 Script executed:

# Check test guidelines for migrations
cat -n src/backend/tests/unit/alembic/test_migration_guidelines.py | head -150

Repository: langflow-ai/langflow

Length of output: 5671


🏁 Script executed:

# Look at another similar migration to see the complete pattern
cat -n src/backend/base/langflow/alembic/versions/59a272d6669a_ensure_trace_flow_id_cascade.py

Repository: langflow-ai/langflow

Length of output: 3489


🏁 Script executed:

# Verify if there are tests for downgrade reversibility
rg "downgrade" --type py src/backend/tests/unit/alembic/ -A 2 -B 2

Repository: langflow-ai/langflow

Length of output: 5985


🏁 Script executed:

# Check if any of these migrations have been tested for rollback
rg "test.*downgrade\|def test.*rollback" --type py

Repository: langflow-ai/langflow

Length of output: 46


Downgrade cannot properly reverse when FK names are unavailable on SQLite.

The downgrade() method returns early when fk_name is None (lines 61-62), leaving the FK with CASCADE semantics instead of restoring the non-CASCADE version. SQLite does not reliably expose FK constraint names via SQLAlchemy's inspector API, making this downgrade path incomplete.

Handle the None case explicitly: either track the constraint differently, store metadata in comments/triggers, or update downgrade to recreate the FK by dropping and recreating it when name lookup fails.

Also applies to: 61-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/backend/base/langflow/alembic/versions/a3f8b2c91d04_add_ondelete_cascade_to_span_trace_id_fk.py`
around lines 23 - 29, The downgrade() currently returns early when
_get_fk_constraint_name(...) yields None (so SQLite leaves the CASCADE FK in
place); update downgrade() to handle fk_name is None by falling back to
inspecting inspector.get_foreign_keys(table_name) to find the FK entry by
constrained_columns and referred_table/columns and then recreate the FK without
ON DELETE CASCADE (i.e., drop and re-add the FK constraint or recreate the table
schema with the non-CASCADE FK) instead of returning; modify
_get_fk_constraint_name/downgrade to use the FK metadata (constrained_columns,
referred_table, referred_columns) to identify and rebuild the constraint when
the inspector cannot provide a name.

Comment on lines +356 to +358
trace_ids = (await session.exec(sm_select(TraceTable.id).where(TraceTable.flow_id == flow_id))).all()
if trace_ids:
await session.exec(delete(SpanTable).where(SpanTable.trace_id.in_(trace_ids)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid materializing all trace IDs before deleting spans.

Loading all IDs into Python and then building a large IN (...) list can degrade badly for large flows. Use a subquery delete directly.

💡 Proposed change
-        trace_ids = (await session.exec(sm_select(TraceTable.id).where(TraceTable.flow_id == flow_id))).all()
-        if trace_ids:
-            await session.exec(delete(SpanTable).where(SpanTable.trace_id.in_(trace_ids)))
+        trace_ids_subquery = sm_select(TraceTable.id).where(TraceTable.flow_id == flow_id)
+        await session.exec(delete(SpanTable).where(SpanTable.trace_id.in_(trace_ids_subquery)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/base/langflow/api/utils/core.py` around lines 356 - 358, The
current code materializes trace IDs by calling
session.exec(sm_select(TraceTable.id)...) and then uses Python-level trace_ids
in SpanTable.trace_id.in_(trace_ids), which will build a huge IN(...) list for
large flows; change to perform a SQL-level subquery delete so the DB deletes
spans directly without loading IDs into Python—replace the two-step pattern with
a single delete against SpanTable where SpanTable.trace_id is in a subquery
selecting TraceTable.id (use the existing
sm_select/TraceTable/SpanTable/delete/session.exec symbols to construct the
subquery so the database executes the deletion).

@YeonghyeonKO
Copy link
Copy Markdown

I just was wondering that when this PR would be merged. This is quite a bug which has to be fixed early.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working community Pull Request from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow deletion fails with ForeignKeyViolation when traces exist — span.trace_id FK missing ON DELETE CASCADE

2 participants