Skip to content

feat: Update deletion trigger to run on statement - BED-8796#100

Open
StephenHinck wants to merge 2 commits into
mainfrom
BED-8796
Open

feat: Update deletion trigger to run on statement - BED-8796#100
StephenHinck wants to merge 2 commits into
mainfrom
BED-8796

Conversation

@StephenHinck

@StephenHinck StephenHinck commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

BloodHound primarily works with PG in batch data, it is rare for the application to delete a single node or edge. By moving the table trigger to delete edges associated with nodes to FOR EACH STATEMENT, batch node deletes will call a single edge deletion, rather than running for every deleted node, improving DB performance across all deletions.

Resolves: BED-8796

Type of Change

  • Chore (a change that does not modify the application functionality)
  • Bug fix (a change that fixes an issue)
  • New feature / enhancement (a change that adds new functionality)
  • Refactor (no behaviour change)
  • Test coverage
  • Build / CI / tooling
  • Documentation

Testing

  • Unit tests added / updated
  • Integration tests added / updated
  • Full test suite run (make test_all with CONNECTION_STRING set)

Screenshots (if appropriate):

Driver Impact

  • PostgreSQL driver (drivers/pg)
  • Neo4j driver (drivers/neo4j)

Checklist

  • Code is formatted
  • All existing tests pass
  • go.mod / go.sum are up to date if dependencies changed

Summary by CodeRabbit

  • Bug Fixes
    • Improved cascade delete behavior so related edges are removed correctly when multiple nodes are deleted at once in PostgreSQL.
    • Updated database-related dependencies for stability and compatibility.
  • Tests
    • Added coverage for batch node deletion to verify only the expected edges are removed.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PostgreSQL delete_node_edges() trigger is converted from FOR EACH ROW to FOR EACH STATEMENT using a transition table (REFERENCING OLD TABLE AS deleted_nodes), replacing per-row OLD.id predicates with a set-based IN (SELECT id FROM deleted_nodes) delete. A new integration test validates the statement-level cascade behavior. Go module dependencies are also bumped.

Changes

Statement-level cascade delete trigger

Layer / File(s) Summary
Trigger rework to statement-level
drivers/pg/query/sql/schema_up.sql
delete_node_edges() function and trigger updated to use FOR EACH STATEMENT with REFERENCING OLD TABLE AS deleted_nodes; edge deletion predicate changed from = OLD.id to IN (SELECT id FROM deleted_nodes); strict attribute removed.
Integration test for cascade behavior
integration/pgsql_delete_cascade_test.go
New manual_integration-tagged test creates a 5-node/4-edge graph, batch-deletes 3 nodes in one statement, and asserts surviving counts via Cypher. Includes countByCypher helper.

Go dependency updates

Layer / File(s) Summary
Dependency version bumps
go.mod
Bumps golang.org/x/term, mattn/go-runewidth, pkg/errors, stretchr/objx, golang.org/x/crypto/sys/text; adds clipperhouse/uax29/v2 v2.2.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • brandonshearin

Poem

🐇 Hoppity-hop, one sweep, not a row,
The trigger now batches each node's final bow.
Transition tables shimmer in SQL's delight,
Edges cascade away, all gone in one night.
The rabbit stamps paws — the test passes bright! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the main change: moving the deletion trigger to statement level.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the required template and includes the change summary, ticket, type, testing, driver impact, and checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch BED-8796

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integration/pgsql_delete_cascade_test.go`:
- Around line 105-107: The cascade-delete assertion is currently querying via
countByCypher on CascadeDeleteNode/CascadeDeleteEdge, which only sees
relationships whose endpoints still exist. Update the pgsql_delete_cascade_test
check to count the edge table directly with a SQL count(*) against the edge
storage so orphaned CascadeDeleteEdge rows are detected; keep the existing
expectation and adjust the helper usage in this test block accordingly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71bb1960-c899-4331-b423-44347be4b163

📥 Commits

Reviewing files that changed from the base of the PR and between 8fbfba4 and f0239e1.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • drivers/pg/query/sql/schema_up.sql
  • go.mod
  • integration/pgsql_delete_cascade_test.go

Comment on lines +105 to +107
if edgeCount := countByCypher(t, ctx, db, "MATCH (:CascadeDeleteNode)-[r:CascadeDeleteEdge]->(:CascadeDeleteNode) RETURN count(r)"); edgeCount != 1 {
t.Fatalf("surviving edge count: got %d, want 1", edgeCount)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm Cypher MATCH translation joins edges to existing nodes (excluding orphans),
# which would make the current edge assertion blind to failed cascades.
rg -nP --type=go -C3 'func .*Query\(' drivers/pg
rg -nP --type=go -C3 'start_id|end_id|join .*node' drivers/pg/query

Repository: SpecterOps/DAWGS

Length of output: 3121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files integration/pgsql_delete_cascade_test.go drivers/pg/query.go drivers/pg/query/format.go drivers/pg/transaction.go | cat

printf '\n== Outline: integration/pgsql_delete_cascade_test.go ==\n'
ast-grep outline integration/pgsql_delete_cascade_test.go --view expanded || true

printf '\n== Outline: drivers/pg/query.go ==\n'
ast-grep outline drivers/pg/query.go --view expanded || true

printf '\n== Outline: drivers/pg/query/format.go ==\n'
ast-grep outline drivers/pg/query/format.go --view expanded || true

printf '\n== Outline: drivers/pg/transaction.go ==\n'
ast-grep outline drivers/pg/transaction.go --view expanded || true

printf '\n== Relevant snippets ==\n'
for f in integration/pgsql_delete_cascade_test.go drivers/pg/query.go drivers/pg/query/format.go drivers/pg/transaction.go; do
  printf '\n--- %s ---\n' "$f"
  wc -l "$f"
done

printf '\n== test file excerpt ==\n'
sed -n '1,220p' integration/pgsql_delete_cascade_test.go

printf '\n== query.go excerpt ==\n'
sed -n '1,220p' drivers/pg/query.go

printf '\n== query/format.go excerpt around relationship upsert ==\n'
sed -n '130,220p' drivers/pg/query/format.go

printf '\n== transaction.go excerpt around Query ==\n'
sed -n '250,340p' drivers/pg/transaction.go

Repository: SpecterOps/DAWGS

Length of output: 17875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== translate package files ==\n'
git ls-files 'cypher/models/pgsql/translate/**/*.go' 'cypher/models/pgsql/translate/*.go' | cat

printf '\n== outlines ==\n'
for f in $(git ls-files 'cypher/models/pgsql/translate/**/*.go' 'cypher/models/pgsql/translate/*.go'); do
  printf '\n--- %s ---\n' "$f"
  ast-grep outline "$f" --view expanded || true
done

printf '\n== search for relationship joins ==\n'
rg -n -C 3 'start_id|end_id|join .*node|relationship|pattern' cypher/models/pgsql/translate

printf '\n== search for MATCH/relationship translation tests ==\n'
rg -n -C 2 'MATCH \(.*\)-\[.*\]->\(.*\)|count\(r\)|start_id|end_id' cypher/models/pgsql/translate test integration cypher || true

Repository: SpecterOps/DAWGS

Length of output: 50375


Count the edge table directly here. MATCH (:CascadeDeleteNode)-[r:CascadeDeleteEdge]->(:CascadeDeleteNode) only matches relationships whose endpoints still exist, so orphaned rows left behind by a broken cascade can be missed. Use a direct select count(*) from edge check instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration/pgsql_delete_cascade_test.go` around lines 105 - 107, The
cascade-delete assertion is currently querying via countByCypher on
CascadeDeleteNode/CascadeDeleteEdge, which only sees relationships whose
endpoints still exist. Update the pgsql_delete_cascade_test check to count the
edge table directly with a SQL count(*) against the edge storage so orphaned
CascadeDeleteEdge rows are detected; keep the existing expectation and adjust
the helper usage in this test block accordingly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant