/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. * * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace OpenSearch.Client { using Containers = List; internal static class BoolQueryAndExtensions { internal static QueryContainer CombineAsMust(this QueryContainer leftContainer, QueryContainer rightContainer) { QueryContainer c; var leftBool = leftContainer.Self()?.Bool; var rightBool = rightContainer.Self()?.Bool; //neither side is a bool, no special handling needed wrap in a bool must if (leftBool == null && rightBool == null) return CreateMustContainer(new Containers { leftContainer, rightContainer }); else if (TryHandleBoolsWithOnlyShouldClauses(leftContainer, rightContainer, leftBool, rightBool, out c)) return c; else if (TryHandleUnmergableBools(leftContainer, rightContainer, leftBool, rightBool, out c)) return c; //neither side is unmergable so neither is a bool with should clauses var mustNotClauses = OrphanMustNots(leftContainer).EagerConcat(OrphanMustNots(rightContainer)); var filterClauses = OrphanFilters(leftContainer).EagerConcat(OrphanFilters(rightContainer)); var mustClauses = OrphanMusts(leftContainer).EagerConcat(OrphanMusts(rightContainer)); var container = CreateMustContainer(mustClauses, mustNotClauses, filterClauses); return container; } /// /// Handles cases where either side is a bool which indicates it can't be merged yet the other side is mergable. /// A side is considered unmergable if its locked (has important metadata) or has should clauses. /// Instead of always wrapping these cases in another bool we merge to unmergable side into to others must clause therefor flattening the /// generated graph /// [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")] // there for clarity private static bool TryHandleUnmergableBools( QueryContainer leftContainer, QueryContainer rightContainer, IBoolQuery leftBool, IBoolQuery rightBool, out QueryContainer c ) { c = null; var leftCantMergeAnd = leftBool != null && !leftBool.CanMergeAnd(); var rightCantMergeAnd = rightBool != null && !rightBool.CanMergeAnd(); if (!leftCantMergeAnd && !rightCantMergeAnd) return false; if (leftCantMergeAnd && rightCantMergeAnd) c = CreateMustContainer(leftContainer, rightContainer); //right can't merge but left can and is a bool so we add left to the must clause of right else if (!leftCantMergeAnd && leftBool != null && rightCantMergeAnd) { leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer); c = leftContainer; } //right can't merge and left is not a bool, we forcefully create a wrapped must container else if (!leftCantMergeAnd && leftBool == null && rightCantMergeAnd) c = CreateMustContainer(leftContainer, rightContainer); //left can't merge but right can and is a bool so we add left to the must clause of right else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool != null) { rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer); c = rightContainer; } //left can't merge and right is not a bool, we forcefully create a wrapped must container else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool == null) c = CreateMustContainer(new Containers { leftContainer, rightContainer }); return c != null; } /// /// Both Sides are bools, but one of them has only should clauses so we should wrap into a new container. /// Unless we know one of the sides is a bool with only a must who's clauses are all bools with only shoulds. /// This is a piece of metadata we set at the bools creation time so we do not have to itterate the clauses on each combination /// In this case we can optimize the generated graph by merging and preventing stack overflows /// private static bool TryHandleBoolsWithOnlyShouldClauses( QueryContainer leftContainer, QueryContainer rightContainer, IBoolQuery leftBool, IBoolQuery rightBool, out QueryContainer c ) { c = null; var leftHasOnlyShoulds = leftBool.HasOnlyShouldClauses(); var rightHasOnlyShoulds = rightBool.HasOnlyShouldClauses(); if (!leftHasOnlyShoulds && !rightHasOnlyShoulds) return false; if (leftContainer.HoldsOnlyShouldMusts && rightHasOnlyShoulds) { leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer); c = leftContainer; } else if (rightContainer.HoldsOnlyShouldMusts && leftHasOnlyShoulds) { rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer); c = rightContainer; } else { c = CreateMustContainer(new Containers { leftContainer, rightContainer }); c.HoldsOnlyShouldMusts = rightHasOnlyShoulds && leftHasOnlyShoulds; } return true; } private static QueryContainer CreateMustContainer(QueryContainer left, QueryContainer right) => CreateMustContainer(new Containers { left, right }); private static QueryContainer CreateMustContainer(List mustClauses) => new QueryContainer(new BoolQuery() { Must = mustClauses.ToListOrNullIfEmpty() }); private static QueryContainer CreateMustContainer( List mustClauses, List mustNotClauses, List filters ) => new QueryContainer(new BoolQuery { Must = mustClauses.ToListOrNullIfEmpty(), MustNot = mustNotClauses.ToListOrNullIfEmpty(), Filter = filters.ToListOrNullIfEmpty() }); private static bool CanMergeAnd(this IBoolQuery boolQuery) => boolQuery != null && !boolQuery.Locked && !boolQuery.Should.HasAny(); private static IEnumerable OrphanMusts(QueryContainer container) { var lBoolQuery = container.Self().Bool; if (lBoolQuery == null) return new[] { container }; return lBoolQuery.Must?.AsInstanceOrToListOrNull(); } private static IEnumerable OrphanMustNots(IQueryContainer container) => container.Bool?.MustNot?.AsInstanceOrToListOrNull(); private static IEnumerable OrphanFilters(IQueryContainer container) => container.Bool?.Filter?.AsInstanceOrToListOrNull(); } }