sql compatible three-valued logic native filters #15058
Conversation
…s.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false
|
coverage failure is related to sql compat mode = false, where almost none of this new code runs in that mode so i guess is expected, will poke around to see if there is anything i can do to make the bot happy though |
| return o -> stringPredicate.apply(null); | ||
| } | ||
|
|
||
| default boolean isNullInputUnknown() |
| }; | ||
| } | ||
|
|
||
| public static ValueMatcher makeAlwaysFalseMatcher(final DimensionSelector selector, boolean multiValue) |
There was a problem hiding this comment.
Javadoc would be helpful here, since it's unclear when to use this one vs allFalse
There was a problem hiding this comment.
added javadocs and also to the constant allFalse/allTrue/allUnknown methods
| * @param includeUnknown mapping for Druid native two state logic system into SQL three-state logic system. If set | ||
| * to true, this method should also return true if the result is 'unknown' to be a match, such | ||
| * as from the input being null valued. Used primarily to allow | ||
| * {@link org.apache.druid.segment.filter.NotFilter} to invert a match in an SQL compliant |
There was a problem hiding this comment.
tiniest of all nits: "a SQL" is better than "an SQL" because "SQL" is pronounced "sequel" 😉
| @Override | ||
| public boolean isNullInputUnknown() | ||
| { | ||
| return !values.contains(null); |
There was a problem hiding this comment.
Cache this? Looks like isNullInputUnknown is called row-by-row in certain cases.
There was a problem hiding this comment.
probably a few others that would be useful to cache too, will try to fix them
| // the list) | ||
| if (values.get(0) == null) { | ||
| return NullDimensionSelectorHolder.NULL_DIMENSION_SELECTOR; | ||
| } |
There was a problem hiding this comment.
Why is this needed — doesn't constant(null) do the same thing?
There was a problem hiding this comment.
oops i think it is not, i think i must have confused myself between the constant method which diverts nulls to the nil selector and the ConstantDimensionSelector which guards against nulls and explodes.
| if (row.size() == 0) { | ||
| return true; | ||
| } | ||
| //noinspection SSBasedInspection |
There was a problem hiding this comment.
I think the SSBasedInspection is complaining that row.size() is called for each iteration rather than being saved in a local variable. Might as well save it?
There was a problem hiding this comment.
cool, i forgot where this was defined and couldn't remember why it warned against it, but that makes sense. looks like a few other places that can also be updated
| public boolean matches() | ||
| public boolean matches(boolean includeUnknown) | ||
| { | ||
| // todo (clint): what to do about includeUnknown |
There was a problem hiding this comment.
We should avoid committing todos.
IMO, column comparison should ignore includeUnknown, i.e. if inputs are x and y it should be treated as x IS NOT DISTINCT FROM y. Just to keep things simple.
There was a problem hiding this comment.
cool, forgot about this todo, will add javadoc to this filter to call out this behavior.
| } | ||
|
|
||
| final ReadableVectorMatch match = filterMatcher.match(VectorMatch.allTrue(baseOffset.getCurrentVectorSize())); | ||
| final ReadableVectorMatch match = filterMatcher.match(VectorMatch.allTrue(baseOffset.getCurrentVectorSize()), |
There was a problem hiding this comment.
this is the main reason i hate using intellij to add parameters with default values for me using the refactor dialog thingo... on the one hand i'm lazy and its nice it can fill the falses in everywhere, but it always mangles everything so idk if i really come out ahead...
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Nice filter you have there... NOT! |
abhishekagarwal87
left a comment
There was a problem hiding this comment.
I am only 20% done but I had some questions about the change is implemented before I proceed further. The PR description too, while explains the problem and the solution, didn't reveal much detail on implementation front.
| if (o == null || o instanceof Object[]) { | ||
| if (includeUnknown && o == null && predicateFactory.isNullInputUnknown()) { | ||
| selection[numRows++] = rowNum; | ||
| } else if (o == null || o instanceof Object[]) { |
There was a problem hiding this comment.
| } else if (o == null || o instanceof Object[]) { | |
| } else if (o instanceof Object[]) { |
shouldn't it be the above?
There was a problem hiding this comment.
the previous case was handling if includeUnknown and predicateFactory.isNullInputUnknown(). If predicateFactory.isNullInputUnknown() is false then we still have to evaluate the predicate against the value, including nulls, so we want to push nulls and arrays into the predicate directly so they don't get wrapped as a single element array like the else clause is doing
There was a problem hiding this comment.
btw, updated the javadocs on isNullInputUnknown of the predicate factory to hopefully clear this up
Updated the PR description to go over the design a bit with some examples, as well as discussing the other designs considered before settling on this one. |
abhishekagarwal87
left a comment
There was a problem hiding this comment.
Looks good to me, overall. I discussed with @clintropolis offline to understand the PR a bit more.
I think the includeUnknown and isNullInputUnknown could be better named for parity. E.g.
argument - includeUnknown
function name - doesNotIncludeUnknown()
or
argument - matchesUnknown
function name - doesNotMatchUnknown()
| { | ||
| this.extractionFn = extractionFn; | ||
| this.values = values; | ||
| this.hasNull = !values.contains(null); |
There was a problem hiding this comment.
| this.hasNull = !values.contains(null); | |
| this.hasNull = values.contains(null); |
| @Override | ||
| public boolean isNullInputUnknown() | ||
| { | ||
| return hasNull; |
There was a problem hiding this comment.
| return hasNull; | |
| return !hasNull; |
There was a problem hiding this comment.
oops yea, i named this bad, will flip it around
| if (includeUnknown) { | ||
| final int[] vector = selector.getRowVector(); | ||
| final int[] inputSelection = mask.getSelection(); | ||
| final int inputSelectionSize = mask.getSelectionSize(); | ||
| final int[] outputSelection = match.getSelection(); | ||
| int outputSelectionSize = 0; | ||
|
|
||
| for (int i = 0; i < inputSelectionSize; i++) { | ||
| final int rowNum = inputSelection[i]; | ||
| if (NullHandling.isNullOrEquivalent(selector.lookupName(vector[rowNum]))) { | ||
| outputSelection[outputSelectionSize++] = rowNum; | ||
| } | ||
| } | ||
| match.setSelectionSize(outputSelectionSize); | ||
| return match; | ||
| } | ||
| return VectorMatch.allFalse(); |
There was a problem hiding this comment.
Would this cause any perf penalty for queries involving not filter?
There was a problem hiding this comment.
yea, there will likely be some, but .. there isn't really anything we can do because the current behavior is incorrect. If the query results don't have to be correct then I'm sure we could make something even faster that doesn't read or check anything 😜
There was a problem hiding this comment.
also in practice, strings and auto columns will be using indexes, which is just one extra bitmap operation to include the null bitmap in most cases, so in practice those should be not a lot different
| public ConstantMultiValueDimensionSelector(List<String> values) | ||
| { | ||
| if (CollectionUtils.isNullOrEmpty(values)) { | ||
| throw new IllegalArgumentException("Use DimensionSelector.constant(null)"); |
There was a problem hiding this comment.
I guess its a defensive check so druidException.defensive could be a better fit.
There was a problem hiding this comment.
ah yeah, updated this when i was adjacent to some code that was still throwing old exceptions, i can change if i push more commits
| { | ||
| return BooleanValueMatcher.of(predicate.apply(null)); | ||
| final Predicate<String> predicate = predicateFactory.makeStringPredicate(); | ||
| return predicate.apply(null) ? ValueMatchers.allTrue() : ValueMatchers.allUnknown(); |
There was a problem hiding this comment.
shouldn't we be checking the value of predicateFactory.isNullInputUnknown() here?
There was a problem hiding this comment.
hmm, good question, I guess we technically should do ValueMatchers.allFalse if isNullInputUnknown is false and the predicate doesn't match null, will update and add test (if test agrees that is what should be happening)
i feel like
I went with idk, naming is hard, hopefully the javadocs make it clear enough the intent of these arguments and functions |
Added I don't love it, but hopefully someday all of these can just go away and we only have "sql compatible Druid" and no other choice. |
…tive-tri-state-filtering
…wards SQL complaint behavior
|
added log messages to log.warn on cluster startup if non-recommended configurations are used: |
| |Property|Description|Default| | ||
| |---|---|---| | ||
| |`druid.generic.useDefaultValueForNull`|Set to `false` to store and query data in SQL compatible mode. When set to `true` (legacy mode), `null` values will be stored as `''` for string columns and `0` for numeric columns.|`false`| | ||
| |`druid.generic.useThreeValueLogic`|Set to `true` to use SQL compatible three-value logic when processing Druid filters when `druid.generic.useDefaultValueForNull` is `false` and `druid.expressions.useStrictBooleans` is `true`. When set to `false` Druid uses 2 value logic for filter processing, even when `druid.generic.useDefaultValueForNull=false` and `druid.expressions.useStrictBooleans` is true.|`true`| |
There was a problem hiding this comment.
Shouldn't this be druid.generic.useThreeValueLogicForNativeFilters? A mouthful, but isn't 3VL still used inside expressions when strict booleans is true and this is false?
Also, nit: consistency between "druid.generic.useDefaultValueForNull=false" vs "druid.expressions.useStrictBooleans is true"
Also, suggest linking to ../querying/sql-data-types.md for more info.
| When `druid.expressions.useStrictBooleans = true`, Druid uses [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic#SQL) for | ||
| [expressions](math-expr.md) evaluation, such as `expression` virtual columns or `expression` filters. | ||
| If `druid.generic.useDefaultValueForNull = false` (in combination with `druid.expressions.useStrictBooleans = true`), Druid also uses three-valued logic for native filters. | ||
| * [`druid.generic.useDefaultValueForNull`](../configuration/index.md#sql-compatible-null-handling) set to false (default), a runtime property which allows NULL values to exist in numeric columns and expressions, and string typed columns to distinguish between NULL and the empty string |
There was a problem hiding this comment.
Clearer to replace "set to false" with "must be set to false"
(& similar comments for other bullets)
|
|
||
| The [`druid.expressions.useStrictBooleans`](../configuration/index.md#expression-processing-configurations) | ||
| runtime property controls Druid's boolean logic mode. For the most SQL compliant behavior, set this to `true` (the default). | ||
| By default, Druid uses [SQL three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic#SQL) for filter processing |
There was a problem hiding this comment.
Hmm, we should add mention about this parameter to querying/filters.md too. This doc is all about SQL, but the same issue affects filters in native queries.
There was a problem hiding this comment.
add link to here from filters.md
…tive-tri-state-filtering
gianm
left a comment
There was a problem hiding this comment.
LGTM, please fix up the doc grammar, although that can be done in a follow-up if you want to avoid another round of CI on this patch.
|
I notice there's just one CI failure right now, in code coverage for non-SQL-compat mode, where most of this new code wouldn't run. It's OK to ignore that one. |
Co-authored-by: Gian Merlino <[email protected]>
* sql compatible tri-state native logical filters when druid.expressions.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false, and new druid.generic.useThreeValueLogicForNativeFilters=true * log.warn if non-default configurations are used to guide operators towards SQL complaint behavior
* sql compatible tri-state native logical filters when druid.expressions.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false, and new druid.generic.useThreeValueLogicForNativeFilters=true * log.warn if non-default configurations are used to guide operators towards SQL complaint behavior
* sql compatible tri-state native logical filters when druid.expressions.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false, and new druid.generic.useThreeValueLogicForNativeFilters=true * log.warn if non-default configurations are used to guide operators towards SQL complaint behavior
changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in apache#15058
…5182) * add native filters for "(filter) is true" and "(filter) is false" changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in #15058 * add tests for default value mode
…ache#15182) * add native filters for "(filter) is true" and "(filter) is false" changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in apache#15058 * add tests for default value mode
…5182) (#15197) * add native filters for "(filter) is true" and "(filter) is false" changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in #15058 * add tests for default value mode
* sql compatible tri-state native logical filters when druid.expressions.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false, and new druid.generic.useThreeValueLogicForNativeFilters=true * log.warn if non-default configurations are used to guide operators towards SQL complaint behavior
…ache#15182) * add native filters for "(filter) is true" and "(filter) is false" changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in apache#15058 * add tests for default value mode
* sql compatible tri-state native logical filters when druid.expressions.useStrictBooleans=true and druid.generic.useDefaultValueForNull=false, and new druid.generic.useThreeValueLogicForNativeFilters=true * log.warn if non-default configurations are used to guide operators towards SQL complaint behavior
…ache#15182) * add native filters for "(filter) is true" and "(filter) is false" changes: * add IsTrueDimFilter, IsFalseDimFilter, and abstract IsBooleanDimFilter for native json filter implementations of `(filter) IS TRUE` and `(filter) IS FALSE` * add IsBooleanFilter for actual filtering logic for these filters, which ignore includeUnknown to always use matches with false for true and !matches with true for false * fix test incorrectly adjusted to wrong answer in apache#15058 * add tests for default value mode

Description
This PR modifies Druid native filters to correctly observe SQL three-valued logic (
true,false,unknown) instead of Druids classic two-state logic, with a newdruid.generic.useThreeValueLogicForNativeFilterswhich defaults to true and alsodruid.expressions.useStrictBooleansis set to true anddruid.generic.useDefaultValueForNullis false (both the defaults in upcoming Druid 28, see #14154).The most prominent example of what is fixed by this is something like the following query
which prior to this change would incorrect match null values, but after behaves in SQL compliant fashion and only matches actual values:
This change has only been done in SQL compatible/strict boolean mode to be consistent with the behavior of Druids native expressions, which already using the correct tri-state logic system if these flags were in place.
Design
The design is pretty-straightforward, involves modifying the 3 interfaces involved in Druid native filter matching,
BitmapColumnIndex,ValueMatcher, andVectorValueMatcher, to accept an additional booleanincludeUnknownargument when computing their matches.The idea is fairly simple, when
includeUnknownisfalse, filters only consider rows matching when the match is definitely true, and whenincludeUnknownistrue, the filters consider both definitely true and also 'unknown' values as matches. This primarily comes into play when aNotFilteris involved, during which we only want to match the values of a filter that are definitely false. So theNotFilterwill flip the value ofincludeUnknown, delegating to the child filter what it actually considers 'unknown', to get matches for bothTRUEandUNKNOWN, which it can then invert to translate into matches that are only `FALSE.For example, the native equality filter, something like
x = 'hello', withincludeUnknownset tofalsewill match all rows that have the value'hello'. For something likex != 'hello', theNotFilterwill call the match methods of the child equality filter withincludeUnknownset to true, which will consider rows with'hello'as matches, and also rows withNULLas matches, which theNotFiltercan then flip to consider all of the rows with non-null values that are not equal to'hello'as the matches.Another example that handles
includeUnknowndifferently (which is not actually present in this PR, but I am planning to add in a follow-up PR) would be something like a native "is true" filter. The "is true" filter considers non-null truthy values as matches regardless of the value ofincludeUnknownsince it doesn't have any "unknown" values, the rows are either definitely true or definitely not true, so something likeis not truethe child filter would still only return the rows that match true, allowing the not filter to correctly match rows with both false and unknown values.For predicate based filters,
DruidPredicateFactoryhas a new method added,which is used to indicate if null inputs to the predicate should be considered as 'unknowns' and automatically match whenever 'includeUnknown' is set to true, or if they should be delegated to the predicate to have it directly match the value, since some predicates do in fact match null values (such as bloom filters, various expression filters, etc).
Rejected Designs
We primarily considered two other designs before moving forward with this one.
Separate unknown matching methods
One approach involved instead of adding an argument to these native matching interfaces to instead have additional methods to split out 'unknown' matching, e.g. adding a
boolean unknown();method to theValueMatcherinterface, and similar for index and vector matchers. In some ways, I actually still kind of prefer this approach because it is nice and tidy and unknown matching is handled completely separately from 'definitely true' matching, however in practice it had a pretty big downside, which is that theVectorValueMatcherimplementations are effectively forced to maintain two vectors, one for the true selection and one for the unknown selection, or alternately iterate the vector twice if the unknowns are asked for, and also greatly increased the complexity of vector matcher implementations. We were also unable to think of any benefit to being able to keep the unknowns separated from the matches, so we went with the argument approach instead.Simulate it in the SQL layer
This approach involved just making the SQL planner introduce the necessary extra native filters required to ensure correct behavior entirely in the SQL planner, and is basically sketched out in #14949. The idea is like for something like
x != 'hello'we plan into a nativeand(not(eq(x, 'hello')), not(null(x)). There are some nice things about this approach, like not having to modify the native layer at all, but ultimately the downsides were worry that it could never be as performant as supporting this directly at the native layer, greatly increased complexity of the filter analysis step in the sql planner, the requirement to introduce a filter optimizer to remove duplicated filters added implicitly to map two to three valued logic, and lastly, the cognitive burden of the SQL layer having a different behavior than queries run directly at the native layer due to the filters which would be added implicitly at the SQL layer to produce correct behavior from the native layer.Release note
Druid filtering now correctly observes SQL three-valued logic (
true,false,unknown) instead of Druids classic two-state logic wheneverdruid.expressions.useStrictBooleans = trueanddruid.generic.useDefaultValueForNull = false. This will potentially change query results when these configuration values are set, particularly when NOT filters are involved, but for the better as queries should behave as one would expect when using SQL.This PR has: