java.lang.Object
com.darkcollective.relix.semantic.graph.JoinObserver

public final class JoinObserver extends Object
Learns provisional EdgeOrigin.LEARNED schema-graph edges from the joins a user actually writes.

Tables often start with no declared foreign keys, but the joins users write surface the relationships anyway. This observer walks query and view trees and captures each cross-side column equality as a candidate relationship the graph does not yet know. The evidence is a property of the join condition, not of the join operator — so a single case ConditionalJoinNode arm covers all seven uniform joins, and a σ o.cid = c.cid (Orders × Customers) yields the very same edge as the equivalent θ-join. The observation points are:

  • every ConditionalJoinNode (θ, the three outer joins, semi, anti, pairwise-∀) — equality conjuncts of its condition split across the two inputs;
  • NaturalJoinNode and CompositionNode — their matched columns (the name-intersection of the two input schemas);
  • AsOfJoinNode — the equality conjuncts of its condition (its partition keys); the single ordering inequality is never an equijoin and is skipped;
  • a SelectionNode over a ProductNode — the equality conjuncts of the σ predicate split across the ×'s two inputs, the same assertion a θ-join makes written differently.

A bare × carries no evidence, and non-equi conditions (band joins, interval overlap) are not representable as positional equijoin endpoints — both are ignored.

Endpoint resolution. Both sides of a candidate must bottom out in something carrying a RelationSymbol: a RelationNode or a relation-only RenameNode over one (whose alias is how the join condition qualifies its columns — this is what makes a self-join observable). A join over a filtered, projected, or aggregated intermediate is unobservable until derived endpoints participate in learning, so such a side yields nothing here.

Bounds. Join type sets the bounds a learned edge carries, never whether it is captured. An outer join is the direct min = 0 signal on its null-supplying side, which coincides with the unconstrained [0..*] default every other join leaves in place — a join alone never bounds fan-out (max) or guarantees a match (min ≥ 1). So every learned edge is [0..*] on both endpoints; the outer-join distinction is real but structurally the default, and is not fabricated into a bound the evidence does not support (examination against candidate keys, item 5, is what sharpens it).

De-noising. A candidate that the graph already carries between the same relations on the same columns is dropped regardless of name — a learned edge must not shadow or duplicate a declared one. Candidates observed repeatedly (the same join written across many queries) collapse to one. The caller is responsible for supplying only the trees of successful analyses; nothing is learned from a query that failed to resolve.

This class is a pure producer: it reads trees and a graph and returns candidate edges. It never mutates the graph, prompts, or persists — recording policy (confirm-or-record, session persistence) is the REPL's, shared with the conversational acquisition path (item 6).

  • Constructor Details

    • JoinObserver

      public JoinObserver(SymbolTable symbols, SchemaGraph existing)
      Parameters:
      symbols - the symbol table trees resolve against; never null
      existing - the graph to de-noise against — a candidate it already carries is not re-proposed; never null (use SchemaGraph.EMPTY)
  • Method Details

    • observe

      public static List<Relationship> observe(SemanticModel model)
      Observes learned edges across every user-written tree of a semantic model — the inline expression of each root query plus every view body — de-noised against the model's own graph.
      Parameters:
      model - an analysed model; only meaningful for a valid analysis, since nothing worth learning comes from a query that failed to resolve
      Returns:
      the distinct candidate edges, in first-seen order; never null
    • observeTrees

      public List<Relationship> observeTrees(Collection<RelNode> trees)
      Observes learned edges across the given trees, de-noised against this observer's existing graph and against one another.
      Parameters:
      trees - the query/view trees to scan; never null
      Returns:
      the distinct candidate edges, in first-seen order; never null