Skip to content
cccskills
FreePublished on GitHub

rfdb-datalog-hash-join-shared-var

Fix silent incorrect results in RFDB Datalog queries that share a variable between two edge atoms on opposite ends (e.g. self-join patterns like `edge(M, C, "T1"), edge(C, M, "T2")`). Use when: (1) a Datalog rule returns rows that look structurally wrong (e.g. a self-loop rule reports hits but pinning M to each reported id refutes the edge existence), (2) Cypher `MATCH (m)-[:R1]->(c)-[:R2]->(m)` returns the same bogus rows (engine-level bug, not Datalog-specific), (3) results change when you bind one variable explicitly vs. leave it free. Root cause: hash- join fast path in `eval_edge_hash_join` / `eval_incoming_hash_join` unconditionally rebinds the non-key Term::Var without checking whether the variable is already bound in the current row.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md4.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

RFDB Datalog hash-join shared-var bug

Problem

RFDB's Datalog engine has a fast path that kicks in once current bindings exceed HASH_JOIN_THRESHOLD: it pulls all edges of a given type and builds HashMap<src, Vec<dst>> (or the inverse for incoming) for O(1) lookups per binding.

The bug: when the other end of the edge is a Term::Var whose name is already bound in the current row (i.e. a shared variable across two atoms), the fast path rebinds it unconditionally. Any value that satisfies the second edge pattern in isolation gets emitted as a "match," even when it contradicts the first atom's bindings.

Repro

edge(M, C, "CONTAINS"), edge(C, M, "SENDS_MESSAGE")

When you want: M contains C AND C sends back to M (self-loop).

What you get with the bug: M contains C AND C sends to *any* M', bleeding ~N false positives.

Constraining M to a concrete id returns 0 rows; free M returns many. That delta is the symptom.

Trigger conditions

  1. Two edge/3 or incoming/3 atoms share a variable, each on opposite sides of the edge direction.
  2. Current bindings for the first atom exceed HASH_JOIN_THRESHOLD (default 256). Below that, nested-loop runs via unify which handles bound vars correctly.
  3. Edge types are constants (hash-join requires const type).

Fix

In packages/rfdb-server/src/datalog/eval.rs, both eval_edge_hash_join and eval_incoming_hash_join must check the non-key Term::Var against bindings.get(var) before accepting a row:

if let Term::Var(var) = dst_term {
    if let Some(existing) = bindings.get(var).and_then(|v| v.as_id()) {
        if existing != dst_id { continue; }
    }
}

(Mirror in eval_incoming_hash_join with src_term / src_id.)

Put the check before bindings.clone() so mismatched rows are skipped without allocation.

Verification

Regression tests in packages/rfdb-server/src/datalog/tests.rs:

  • test_hash_join_shared_var_across_two_edges — edge/3 self-join.
  • test_incoming_hash_join_shared_var — incoming/3 self-join.

Both build a graph with n = HASH_JOIN_THRESHOLD + 10 triples where exactly one pair forms a true round-trip. Without the fix, the rule returns n rows; with the fix, 1.

How we found it

Adding MESSAGE_TYPE → CONTAINS → CALL edges to the BEAM graph and writing a Datalog guarantee for handler self-loops surfaced 13 hits on Ichi. Drilling into each "hit" M by binding it to a literal id showed zero incoming CONTAINS-that-sends-back edges. Reducing to the minimal repro edge(A, B, "CONTAINS"), edge(B, A, "CONTAINS") on a tree-shaped graph produced 15 spurious symmetric pairs — a tree has zero symmetric CONTAINS by construction. Cypher showed the same rows, ruling out the Datalog reorder/parser layer and pointing at the shared join engine.

Notes

  • Nested-loop path (via unify) is correct — the bug is isolated to the hash-join fast path.
  • eval_negation_hash_join (for \+ edge(X, _, "T")) does not have the same bug because it only projects one variable by design; the other side is always _ / Wildcard.
  • If you ever add a third variant of hash-join (e.g. for path/2 transitive closure), port the same check.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Fix Elixir/Erlang AST processing bugs in Grafema beam-analyzer. Use when: (1) Elixir parser returns MODULE node but 0 functions/calls — body nesting issue, (2) Erlang parser crashes with "cannot convert list to string" on OTP 26+ — location format changed from integer to keyword list, (3) pipe operator |> creates spurious CALL nodes instead of desugared function calls — clause ordering bug, (4) multi-module .ex files return only the first module — missing __block__ handler, (5) installing Erlang/Elixir on macOS with outdated Xcode/Clang.

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

Fires after fixing any non-trivial bug or regression. Asks: could the graph have caught this as a guarantee? Pairs with reflection-in-and-on-action — picks up after "earliest catchable signal" and asks the next question: was that signal expressible in graph? Triggers: (1) after any non-trivial bug fix is verified working, (2) after a regression report (something used to work, broke), (3) during step 6 (knowledge extraction) of the workflow, (4) when reflection-on-action surfaces a "would have been catchable" signal. Outcome is a triage decision (graph-reachable? rule expressible? rule sound?) and either a draft Linear issue + guarantee proposal, or a recorded "graph capability gap" note. Never auto-creates guarantees.

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

Fix Playwright automation failures against code-server (VS Code in browser). Use when: (1) trust dialog blocks all clicks — "monaco-dialog-modal-block intercepts pointer events", (2) button:has-text() finds wrong buttons behind modal dialog, (3) keyboard shortcuts don't work — Meta vs Control inconsistency, (4) VS Code extension activity bar icon not found by aria-label, (5) panels show placeholder text despite extension being "Connected". Covers trust dialog dismissal, keyboard shortcut hybrid mode, extension panel selectors, and database connection verification for Grafema extension.

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

Systematic methodology for achieving 100% backward dataflow reachability in a new language. Create gauntlet fixture, write trace, diagnose gaps, fix analyzer/algorithm, iterate to 100%. Language-agnostic process. Use when: (1) adding a new language to Grafema, (2) auditing dataflow coverage for existing language, (3) user says "/dataflow-gauntlet".

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

Fix docker exec hanging when starting background processes (servers, daemons) inside containers. Use when: (1) docker exec never returns despite using & or nohup, (2) background server started in container causes docker exec to hang indefinitely, (3) env_startup_command in SWE-bench or similar frameworks times out, (4) setsid/disown needed for proper process detachment in Docker. Root cause: docker exec tracks ALL processes in the exec session, not just the top-level PID.

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

Fix Node.js CLI tools crashing inside Docker containers when host-installed node_modules require a newer Node version than the container provides. Use when: (1) "SyntaxError: Invalid regular expression flags" with /v flag in string-width or similar packages, (2) node_modules installed on host with Node 20+ but container has Node 18, (3) `npm install` with file: protocol creates symlinks that break inside Docker, (4) pnpm workspace packages become broken symlinks in containers. Covers version detection, Node binary mounting, and npm install strategies for cross-version compatibility.

The English summary is being prepared. Showing the original description.

Disentinel/grafema35Updated Aug 24, 2026

See all skills by Disentinel

Report a problem with this skill