class FilterSpoolMergeToSortedIndexSpoolRule extends RelRule<FilterSpoolMergeToSortedIndexSpoolRule.Config> {
    /** Instance. */
    public static final RelOptRule INSTANCE = Config.DEFAULT.toRule();

    /** */
    private FilterSpoolMergeToSortedIndexSpoolRule(Config cfg) {
        super(cfg);
    }

    /** {@inheritDoc} */
    @Override public void onMatch(RelOptRuleCall call) {<FILL_FUNCTION_BODY>}

    /** */
    @SuppressWarnings("ClassNameSameAsAncestorName")
    @Value.Immutable
    public interface Config extends RelRule.Config {
        /** */
        Config DEFAULT = ImmutableFilterSpoolMergeToSortedIndexSpoolRule.Config.of()
            .withDescription("FilterSpoolMergeToSortedIndexSpoolRule")
            .withOperandFor(IgniteFilter.class, IgniteTableSpool.class);

        /** Defines an operand tree for the given classes. */
        default Config withOperandFor(Class<? extends Filter> filterClass, Class<? extends Spool> spoolClass) {
            return withOperandSupplier(
                o0 -> o0.operand(filterClass)
                    .oneInput(o1 -> o1.operand(spoolClass)
                        .anyInputs()
                    )
            )
                .as(Config.class);
        }

        /** {@inheritDoc} */
        @Override default FilterSpoolMergeToSortedIndexSpoolRule toRule() {
            return new FilterSpoolMergeToSortedIndexSpoolRule(this);
        }
    }
}

        final IgniteFilter filter = call.rel(0);
        final IgniteTableSpool spool = call.rel(1);

        RelOptCluster cluster = spool.getCluster();

        RelTraitSet trait = spool.getTraitSet();
        CorrelationTrait filterCorr = TraitUtils.correlation(filter);

        if (filterCorr.correlated())
            trait = trait.replace(filterCorr);

        RelNode input = spool.getInput();

        RelCollation inCollation = TraitUtils.collation(input);

        List<SearchBounds> bounds = RexUtils.buildSortedSearchBounds(
            cluster,
            inCollation,
            filter.getCondition(),
            spool.getRowType(),
            null
        );

        if (F.isEmpty(bounds))
            return;

        RelCollation traitCollation;
        RelCollation searchCollation;

        if (inCollation == null || inCollation.isDefault()) {
            // Create collation by index bounds.
            List<Integer> equalsFields = new ArrayList<>(bounds.size());
            List<Integer> otherFields = new ArrayList<>(bounds.size());

            // First, add all equality filters to collation, then add other fields.
            for (int i = 0; i < bounds.size(); i++) {
                if (bounds.get(i) != null)
                    (bounds.get(i).type() == SearchBounds.Type.EXACT ? equalsFields : otherFields).add(i);
            }

            searchCollation = traitCollation = TraitUtils.createCollation(F.concat(true, equalsFields, otherFields));
        }
        else {
            // Create search collation as a prefix of input collation.
            traitCollation = inCollation;

            Set<Integer> searchKeys = Ord.zip(bounds).stream()
                .filter(v -> v.e != null)
                .map(v -> v.i)
                .collect(Collectors.toSet());

            List<RelFieldCollation> collationFields = inCollation.getFieldCollations().subList(0, searchKeys.size());

            assert searchKeys.containsAll(collationFields.stream().map(RelFieldCollation::getFieldIndex)
                .collect(Collectors.toSet())) : "Search condition should be a prefix of collation [searchKeys=" +
                searchKeys + ", collation=" + inCollation + ']';

            searchCollation = RelCollations.of(collationFields);
        }

        RelNode res = new IgniteSortedIndexSpool(
            cluster,
            trait.replace(traitCollation),
            convert(input, input.getTraitSet().replace(traitCollation)),
            searchCollation,
            filter.getCondition(),
            bounds
        );

        call.transformTo(res);
    
<no_super_class>