anchor.diff:        printfn "Can't resolve table for %A" xs
         None
   
  -  member x.AddJoinedTablesOrAliases t1 t2 =
  -    match ((x.ResolveTable t1), (x.ResolveTable t2)) with
  -    | Some a, Some b ->
  -      let t1 = [a; List.last t1]
  -      let t2 = [b; List.last t2]
  -      let lhs = String.concat "." t1
  -      let rhs = String.concat "." t2
  -      let (lhs, rhs) =
  -        if a < b then (lhs, rhs) else (rhs, lhs)
  -
  -      let (a, b) = if a < b then a, b else b, a
  -      printfn "CONNECT %A = %A" t1 t2
  -      ignore <| x.condConnections.Add (a, b, sprintf "%s = %s" lhs rhs)
  +  member x.AddJoinedTablesOrAliases col_x col_y =
  +    match ((x.ResolveTable col_x), (x.ResolveTable col_y)) with
  +    | Some table_x, Some table_y ->
  +      // Column references now fully qualified
  +      let col_x = [table_x; List.last col_x] |> String.concat "."
  +      let col_y = [table_y; List.last col_y] |> String.concat "."
  +
  +      // Normalize order, to avoid repeating joins we already know about
  +      let (col_x, col_y) =
  +        if table_x < table_y then (col_x, col_y) else (col_y, col_x)
  +      let (table_x, table_y) = if table_x < table_y then table_x, table_y else table_y, table_x
  +
  +      ignore <| x.condConnections.Add (table_x, table_y, sprintf "%s = %s" col_x col_y)
       | _, _ -> ()
   
     member x.AddConditions(bexpr:BooleanExpression) =
  
positive.code:       
    static member GetName(mult:MultiPartIdentifier) : string list  =
      match mult with
      | MultiPartIdentifier.SchemaObjectName(so) -> Ops.GetName so
      | MultiPartIdentifier.Base(count, identifiers) -> identifiers |> List.map Ops.GetName 
        
    static member GetName(so:SchemaObjectName) : string list  =
      match so with
      | SchemaObjectName.ChildObjectName(baseIdentifier, childIdentifier, count, databaseIdentifier, identifiers, schemaIdentifier, serverIdentifier) -> failwith "Not implemented yet"
      | SchemaObjectName.SchemaObjectNameSnippet(baseIdentifier, count, databaseIdentifier, identifiers, schemaIdentifier, script, serverIdentifier) -> failwith "Not implemented yet"
      | SchemaObjectName.Base(Identifiers=identifiers) -> 
        identifiers |> List.map Ops.GetName 
    
    static member GetName(idOrVal:IdentifierOrValueExpression) : string  =
      match idOrVal with
      | IdentifierOrValueExpression.IdentifierOrValueExpression(Identifier=Some(ident)) ->
        Ops.GetName ident
      | _ -> failwith "Can't get name of expr without identifier"
  
    static member GetName(so:SchemaObjectNameOrValueExpression) : string list  =
      match so with
      | SchemaObjectNameOrValueExpression.SchemaObjectNameOrValueExpression(SchemaObjectName=Some(son)) ->
        Ops.GetName(son)
      | _ -> failwith "Can't get name, missing schema object"
  
    static member GetName(tra:TableReferenceWithAlias) : string option * string list =
      match tra with
      | TableReferenceWithAlias.NamedTableReference(Alias=alias; SchemaObject=Some(schemaObject)) -> 
        (alias |> Option.map Ops.GetName), (Ops.GetName(schemaObject))
      | _ -> failwith "Not implemented yet"
  
  let getScalarColRefs (sexpr:ScalarExpression) : (string list) list =
    let colRefs = ResizeArray<_>()
    sexpr.ToCs().Accept(
  
negative.code:       (TableReferenceWithAlias.NamedTableReference
          (Alias=alias; SchemaObject=Some
            (schemaObject))) ->
            let aliasName = alias |> Option.map (ExprUtils.GetName)
            let name = ExprUtils.GetName schemaObject |> String.concat "."
            ctx.AddMapping (name, aliasName)
  
    | TableReference.JoinParenthesisTableReference(Some(join)) ->
  