Skip to content

Engine: multi-clause rules silently return empty or malformed results when clauses share a derived relation #91

Closed

Description

@jsam

Summary

Some multi-clause rules evaluate to an empty relation with no error, and one variant returns rows with the wrong number of columns. Single-clause versions of the same rules work. Found while validating the Verified Completions rule pack (#81) on the real engine: the pack's 15-clause conflict relation silently returned zero rows while sibling relations (violation, tension) worked.

This is a wrong-results bug, not a crash: nothing is logged, the rules register fine, and the query just returns "No results."

Repro 1: union goes silently empty

.kg create bug_union_empty
.kg use bug_union_empty
+claim(id: string, entity: string, attr: string, val: string)
+claim_modality(id: string, modality: string)
+link(x: string, y: string)
+claim[("c1", "e1", "attr", "v1"), ("c2", "e2", "attr", "v2"), ("c5", "anna_1", "distinct_from", "anna_2")]
+claim_modality[("c1", "asserted"), ("c2", "asserted"), ("c5", "asserted")]
+link[("e1", "e2")]
+active(C, E, A, V) <- claim(C, E, A, V), claim_modality(C, "asserted")
+r("bridge", C1, C2) <- active(C1, E1, A1, V1), active(C2, E2, A2, V2), link(E1, E2)
+r("solo", C, C) <- active(C, E1, "distinct_from", E2)
?r(K, C1, C2)

Expected: ("bridge","c1","c2") and ("solo","c5","c5"). Actual: No results.

Register only the bridge clause and it returns its row. Register only the solo clause and it returns its row. Together: empty. Order does not matter, and the poisoning is retroactive: if you query between registrations, the first clause's rows appear, then adding the second clause makes the whole relation empty.

Repro 2: malformed output (wrong arity)

.kg create bug_union_arity
.kg use bug_union_arity
+ca(c: string, e: string)
+ca[("c1", "e1"), ("c2", "e1")]
+a(C, E) <- ca(C, E)
+r("joined", C1, C2) <- a(C1, E), a(C2, E), C1 != C2
+r("solo", C, C) <- a(C, E)
?r(K, C1, C2)

Expected: the joined pair plus solo rows, 3 columns. Actual: 8 rows with 4 columns (col0..col3), which looks like an unreduced intermediate cross product leaking out as the query result.

What we narrowed down (each line is a tested variant)

Works:

  • Two clauses over base relations only, any shape, any order
  • Two clauses over the same derived relation when the solo clause has no constant in its body atom (r("solo", C, C) <- active(C, E, A, V))
  • Bridge-join clause over a derived relation + solo clause over a DIFFERENT relation
  • Derived relation defined via projection or via join - both fine in the working combos
  • Three clauses over 2-column derived + base relations (projection case)

Fails:

  • Repro 1: bridge-join clause + solo clause with a string constant in a body atom, both over the same 4-column derived relation (silently empty)
  • Same shape with the bridge as coentity, same_as, or a base link relation (silently empty)
  • Repro 2: self-join + != on head variables + solo clause, 2-column derived relation (malformed 4-column output)

Suspects worth checking: union plan compilation when clauses share a derived relation subplan with different binding patterns (subplan sharing / code generator), and constant-filter pushdown inside one branch of a union.

Impact

Any user-written multi-clause rule can silently return wrong (empty or malformed) results depending on body shape. For a reasoning engine this is the worst failure mode: no error, plausible-looking absence of results.

Workaround (verified): keep one clause per derived relation and union only at a final reporting layer where every clause references a distinct relation. The Verified Completions pack is being restructured this way in #81.

Repro scripts are deterministic and take under a second each; happy to add them as .iql snapshot tests in a category like examples/iql/12_errors or a new regression category once the fix lands.

Activity

  1. added
    bugSomething isn't working
    backendBackend/server related
    verified-completionsVerified Completions: OpenAI-compatible endpoint with consistency checking
    on Jul 26, 2026
  2. jsam commented on Jul 26, 2026

    @jsam
    ContributorAuthor

    Additional data point: the bug is NOT in the optimizer passes. Repro 1 still returns empty with all three flags disabled in config:

    [optimization]
    enable_join_planning = false
    enable_sip_rewriting = false
    enable_subplan_sharing = false
    

    Same result as with defaults. So the problem is in core rule/union compilation or the code generator, not in join planning, SIP rewriting, or subplan sharing.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend/server relatedbugSomething isn't workingverified-completionsVerified Completions: OpenAI-compatible endpoint with consistency checking

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions