@Override
    public List<DataRetentionDeleteSummary> getRetentionDeletionSummary(
            final DataRetentionRules rules,
            final FindDataRetentionImpactCriteria criteria) {

        final List<DataRetentionDeleteSummary> result;

        final List<DataRetentionRule> activeRules = Optional.ofNullable(rules)
                .flatMap(rules2 -> Optional.ofNullable(rules2.getRules()))
                .map(allRules -> allRules.stream()
                        .filter(DataRetentionRule::isEnabled)
                        .collect(Collectors.toList()))
                .orElse(Collections.emptyList());

        if (!activeRules.isEmpty()) {

            CaseConditionStep<Integer> ruleNoCaseConditionStep = null;
            final Map<Integer, DataRetentionRule> numberToRuleMap = new HashMap<>();
            // Order is critical here as we are building a case statement
            // The highest priority rules first, i.e. the largest rule number
            final Map<Integer, Condition> ruleNoToConditionMap = activeRules.stream()
                    .collect(Collectors.toMap(
                            DataRetentionRule::getRuleNumber,
                            rule ->
                                    expressionMapper.apply(rule.getExpression())));

            final List<Condition> orConditions = activeRules.stream()
                    .map(rule -> ruleNoToConditionMap.get(rule.getRuleNumber()))
                    .collect(Collectors.toList());

            for (DataRetentionRule rule : activeRules) {
                numberToRuleMap.put(rule.getRuleNumber(), rule);
                final Condition ruleCondition = ruleNoToConditionMap.get(rule.getRuleNumber());

                // We are not interested in counts for stuff being kept forever as they will never
                // be deleted.
                final Integer caseResult = rule.isForever()
                        ? null
                        : rule.getRuleNumber();

                if (ruleNoCaseConditionStep == null) {
                    ruleNoCaseConditionStep = DSL.when(ruleCondition, caseResult);
                } else {
                    ruleNoCaseConditionStep.when(ruleCondition, caseResult);
                }
            }
            // If none of the rules matches then we don't to delete so return false
            final Field<Integer> ruleNoCaseField = ruleNoCaseConditionStep.otherwise((Field<Integer>) null);

            final byte statusIdDeleted = MetaStatusId.getPrimitiveValue(Status.DELETED);

            final Field<Integer> ruleNoField = DSL.field("rule_no", Integer.class);
            final Field<String> feedNameField = DSL.field("feed_name", String.class);
            final Field<String> typeNameField = DSL.field("type_name", String.class);
            final Field<Long> metaCreateTimeField = DSL.field("meta_create_time_ms", Long.class);

            activeRules.forEach(rule ->
                    validateExpressionTerms(rule.getExpression()));

            final boolean requiresMetaProcessorTable = rulesContainField(
                    activeRules,
                    MetaFields.PIPELINE.getFldName(),
                    MetaFields.PIPELINE_NAME.getFldName());

            return JooqUtil.contextResult(metaDbConnProvider,
                            context -> {
                                var fromClause = meta
                                        .straightJoin(metaFeed).on(meta.FEED_ID.eq(metaFeed.ID))
                                        .straightJoin(metaType).on(meta.TYPE_ID.eq(metaType.ID));
                                // If any of the rules have a predicate on the Pipeline field then we need to
                                // add the join to meta_processor
                                if (requiresMetaProcessorTable) {
                                    fromClause = fromClause.leftOuterJoin(metaProcessor)
                                            .on(meta.PROCESSOR_ID.eq(metaProcessor.ID));
                                }
                                // Get all meta records that are impacted by a rule and for each determine
                                // which rule wins and get its rule number, along with feed and type
                                // The OR condition is here to try and help the DB use indexes.
                                final var detailTable = context
                                        .select(
                                                metaFeed.NAME.as(feedNameField),
                                                metaType.NAME.as(typeNameField),
                                                ruleNoCaseField.as(ruleNoField),
                                                meta.CREATE_TIME.as(metaCreateTimeField))
                                        .from(fromClause)
                                        .where(meta.STATUS.notEqual(statusIdDeleted))
//                                        .and(ruleNoCaseField.isNotNull()) // only want data that WILL be deleted
                                        .and(DSL.or(orConditions)) // Here to help use indexes
                                        .and(getFilterCriteriaCondition(criteria)) // UI filtering
                                        .asTable("detail");

                                final List<Condition> ruleAgeConditions = getRuleAgeConditions(
                                        activeRules,
                                        ruleNoField,
                                        metaCreateTimeField,
                                        detailTable);

                                // Now get counts grouped by feed, type and rule
                                final var query = context
                                        .select(
                                                detailTable.field(feedNameField),
                                                detailTable.field(typeNameField),
                                                detailTable.field(ruleNoField),
                                                DSL.count())
                                        .from(detailTable)
                                        // Ignore rows where the effective rule is a forever one as they
                                        // will never be deleted
                                        .where(detailTable.field(ruleNoField).isNotNull())
                                        // Only include rows that are beyond the rules' retention period
                                        .and(DSL.or(ruleAgeConditions))
                                        .groupBy(
                                                detailTable.field(ruleNoField),
                                                detailTable.field(feedNameField),
                                                detailTable.field(typeNameField));

                                // Dump the query in case we want to run it in a mysql shell.
                                LOGGER.debug("query:\n{}", query);

                                return query.fetch();
                            })
                    .map(record -> {
                        int ruleNo = record.get(ruleNoField);
                        return new DataRetentionDeleteSummary(
                                record.get(feedNameField),
                                record.get(typeNameField),
                                ruleNo,
                                numberToRuleMap.get(ruleNo).getName(),
                                (int) record.get(DSL.count().getName()));
                    });
        } else {
            // No rules so no point running a query
            result = Collections.emptyList();
        }
        return result;
    }
Method currently only here for test purposes as a means of bulk loading data.
stroom-meta/stroom-meta-impl-db/src/main/java/stroom/meta/impl/db/MetaDaoImpl.java