class JoinPlan extends AbstractJoinPlan {

    private final boolean isFiltered;
    private final boolean rewriteFilterOnOuterJoinToInnerJoinDone;
    private final boolean lookUpJoinRuleApplied;
    private final boolean moveConstantJoinConditionRuleApplied;

    public JoinPlan(LogicalPlan lhs,
                    LogicalPlan rhs,
                    JoinType joinType,
                    @Nullable Symbol joinCondition,
                    boolean isFiltered,
                    boolean rewriteFilterOnOuterJoinToInnerJoinDone,
                    boolean lookUpJoinRuleApplied) {
        this(lhs, rhs, joinType, joinCondition, isFiltered, rewriteFilterOnOuterJoinToInnerJoinDone, lookUpJoinRuleApplied, false);
    }

    @VisibleForTesting
    public JoinPlan(LogicalPlan lhs,
                    LogicalPlan rhs,
                    JoinType joinType,
                    @Nullable Symbol joinCondition) {
        this(lhs, rhs, joinType, joinCondition, false, false, false, false);
    }

    private JoinPlan(LogicalPlan lhs,
                    LogicalPlan rhs,
                    JoinType joinType,
                    @Nullable Symbol joinCondition,
                    boolean isFiltered,
                    boolean rewriteFilterOnOuterJoinToInnerJoinDone,
                    boolean lookUpJoinRuleApplied,
                    boolean moveConstantJoinConditionRuleApplied) {
        super(lhs, rhs, joinCondition, joinType);
        this.isFiltered = isFiltered;
        this.rewriteFilterOnOuterJoinToInnerJoinDone = rewriteFilterOnOuterJoinToInnerJoinDone;
        this.lookUpJoinRuleApplied = lookUpJoinRuleApplied;
        this.moveConstantJoinConditionRuleApplied = moveConstantJoinConditionRuleApplied;
    }

    public boolean isLookUpJoinRuleApplied() {
        return lookUpJoinRuleApplied;
    }

    public boolean isFiltered() {
        return isFiltered;
    }

    public boolean isRewriteFilterOnOuterJoinToInnerJoinDone() {
        return rewriteFilterOnOuterJoinToInnerJoinDone;
    }

    public boolean moveConstantJoinConditionRuleApplied() {
        return moveConstantJoinConditionRuleApplied;
    }

    public JoinPlan withMoveConstantJoinConditionRuleApplied(boolean moveConstantJoinConditionRuleApplied) {
        return new JoinPlan(
            lhs,
            rhs,
            joinType,
            joinCondition,
            isFiltered,
            rewriteFilterOnOuterJoinToInnerJoinDone,
            lookUpJoinRuleApplied,
            moveConstantJoinConditionRuleApplied);
    }

    @Override
    public ExecutionPlan build(DependencyCarrier dependencyCarrier,
                               PlannerContext plannerContext,
                               Set<PlanHint> planHints,
                               ProjectionBuilder projectionBuilder,
                               int limit,
                               int offset,
                               @Nullable OrderBy order,
                               @Nullable Integer pageSizeHint,
                               Row params,
                               SubQueryResults subQueryResults) {
        throw new UnsupportedOperationException(
            "JoinPlan cannot be build, it needs to be converted to a NestedLoop/HashJoin");
    }

    @Override
    public <C, R> R accept(LogicalPlanVisitor<C, R> visitor, C context) {
        return visitor.visitJoinPlan(this, context);
    }

    @Override
    public LogicalPlan pruneOutputsExcept(SequencedCollection<Symbol> outputsToKeep) {
        LinkedHashSet<Symbol> lhsToKeep = new LinkedHashSet<>();
        LinkedHashSet<Symbol> rhsToKeep = new LinkedHashSet<>();
        for (Symbol outputToKeep : outputsToKeep) {
            SymbolVisitors.intersection(outputToKeep, lhs.outputs(), lhsToKeep::add);
            SymbolVisitors.intersection(outputToKeep, rhs.outputs(), rhsToKeep::add);
        }
        if (joinCondition != null) {
            SymbolVisitors.intersection(joinCondition, lhs.outputs(), lhsToKeep::add);
            SymbolVisitors.intersection(joinCondition, rhs.outputs(), rhsToKeep::add);
        }
        LogicalPlan newLhs = lhs.pruneOutputsExcept(lhsToKeep);
        LogicalPlan newRhs = rhs.pruneOutputsExcept(rhsToKeep);
        if (newLhs == lhs && newRhs == rhs) {
            return this;
        }
        return new JoinPlan(
            newLhs,
            newRhs,
            joinType,
            joinCondition,
            isFiltered,
            rewriteFilterOnOuterJoinToInnerJoinDone,
            lookUpJoinRuleApplied,
            moveConstantJoinConditionRuleApplied
        );
    }

    @Override
    public void print(PrintContext printContext) {
        printContext
            .text("Join[")
            .text(joinType.toString());
        if (joinCondition != null) {
            printContext
                .text(" | ")
                .text(joinCondition.toString());
        }
        printContext.text("]");
        printStats(printContext);
        printContext.nest(Lists.map(sources(), x -> x::print));
    }

    @Override
    public LogicalPlan replaceSources(List<LogicalPlan> sources) {<FILL_FUNCTION_BODY>}
}

        return new JoinPlan(
            sources.get(0),
            sources.get(1),
            joinType,
            joinCondition,
            isFiltered,
            rewriteFilterOnOuterJoinToInnerJoinDone,
            lookUpJoinRuleApplied,
            moveConstantJoinConditionRuleApplied
        );
    
<methods>public Map<io.crate.planner.operators.LogicalPlan,io.crate.expression.symbol.SelectSymbol> dependencies() ,public io.crate.expression.symbol.Symbol joinCondition() ,public io.crate.sql.tree.JoinType joinType() ,public io.crate.planner.operators.LogicalPlan lhs() ,public List<io.crate.expression.symbol.Symbol> outputs() ,public List<io.crate.metadata.RelationName> relationNames() ,public io.crate.planner.operators.LogicalPlan rhs() ,public List<io.crate.planner.operators.LogicalPlan> sources() ,public boolean supportsDistributedReads() <variables>protected final non-sealed io.crate.expression.symbol.Symbol joinCondition,protected final non-sealed io.crate.sql.tree.JoinType joinType,protected final non-sealed io.crate.planner.operators.LogicalPlan lhs,protected final non-sealed io.crate.planner.operators.LogicalPlan rhs