Skip to content

Commit 79847b9

Browse files
authored
Implement disjunctive normal form (DNF) transform. (#3493)
* Implement disjunctive normal form (DNF) transform. * Add query engine test (full collection scan). * Remove outdated commented line. * Remove `withAddedFilter`. Callers can use `withAddedFilters` with a singlton list * Address comments. * Address comments. * Simplify the distribution code. * Remove unused import. * Address comments.
1 parent 6e93719 commit 79847b9

File tree

8 files changed

+792
-11
lines changed

8 files changed

+792
-11
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,13 @@ public boolean isDisjunction() {
7878
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
7979
*/
8080
public boolean isFlatConjunction() {
81-
if (operator != Operator.AND) {
82-
return false;
83-
}
81+
return isFlat() && isConjunction();
82+
}
83+
84+
/**
85+
* Returns true if this filter does not contain any composite filters. Returns false otherwise.
86+
*/
87+
public boolean isFlat() {
8488
for (Filter filter : filters) {
8589
if (filter instanceof CompositeFilter) {
8690
return false;
@@ -89,6 +93,15 @@ public boolean isFlatConjunction() {
8993
return true;
9094
}
9195

96+
/**
97+
* Returns a new composite filter that contains all filter from `this` plus all the given filters.
98+
*/
99+
public CompositeFilter withAddedFilters(List<Filter> otherFilters) {
100+
List<Filter> mergedFilters = new ArrayList<>(filters);
101+
mergedFilters.addAll(otherFilters);
102+
return new CompositeFilter(mergedFilters, operator);
103+
}
104+
92105
/**
93106
* Performs a depth-first search to find and return the first FieldFilter in the composite filter
94107
* that satisfies the condition. Returns {@code null} if none of the FieldFilters satisfy the
@@ -134,11 +147,9 @@ public boolean matches(Document doc) {
134147
public String getCanonicalId() {
135148
// TODO(orquery): Add special case for flat AND filters.
136149

137-
List<String> canonicalIds = new ArrayList<>();
138-
for (Filter filter : filters) canonicalIds.add(filter.getCanonicalId());
139150
StringBuilder builder = new StringBuilder();
140151
builder.append(isConjunction() ? "and(" : "or(");
141-
TextUtils.join(",", canonicalIds);
152+
builder.append(TextUtils.join(",", filters));
142153
builder.append(")");
143154
return builder.toString();
144155
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public FieldPath getFirstInequalityField() {
182182

183183
@Override
184184
public String toString() {
185-
return field.canonicalString() + " " + operator + " " + Values.canonicalId(value);
185+
return getCanonicalId();
186186
}
187187

188188
@Override

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.firebase.firestore.model.Values.isArray;
1818
import static com.google.firebase.firestore.util.Assert.fail;
1919
import static com.google.firebase.firestore.util.Assert.hardAssert;
20+
import static com.google.firebase.firestore.util.LogicUtils.getDnfTerms;
2021
import static com.google.firebase.firestore.util.Util.diffCollections;
2122
import static com.google.firebase.firestore.util.Util.repeatSequence;
2223
import static java.lang.Math.max;
@@ -45,7 +46,6 @@
4546
import com.google.firebase.firestore.model.SnapshotVersion;
4647
import com.google.firebase.firestore.model.TargetIndexMatcher;
4748
import com.google.firebase.firestore.util.Logger;
48-
import com.google.firebase.firestore.util.LogicUtils;
4949
import com.google.firestore.admin.v1.Index;
5050
import com.google.firestore.v1.StructuredQuery;
5151
import com.google.firestore.v1.Value;
@@ -330,7 +330,7 @@ private List<Target> getSubTargets(Target target) {
330330
} else {
331331
// There is an implicit AND operation between all the filters stored in the target.
332332
List<Filter> dnf =
333-
LogicUtils.DnfTransform(
333+
getDnfTerms(
334334
new CompositeFilter(
335335
target.getFilters(), StructuredQuery.CompositeFilter.Operator.AND));
336336
for (Filter term : dnf) {

firebase-firestore/src/main/java/com/google/firebase/firestore/util/LogicUtils.java

Lines changed: 278 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@
1414

1515
package com.google.firebase.firestore.util;
1616

17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
1719
import com.google.firebase.firestore.core.CompositeFilter;
20+
import com.google.firebase.firestore.core.FieldFilter;
1821
import com.google.firebase.firestore.core.Filter;
22+
import com.google.firestore.v1.StructuredQuery.CompositeFilter.Operator;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
1925
import java.util.Collections;
2026
import java.util.List;
2127

@@ -24,6 +30,265 @@
2430
* complex filters used in queries.
2531
*/
2632
public class LogicUtils {
33+
34+
/** Asserts that the given filter is a FieldFilter or CompositeFilter. */
35+
private static void assertFieldFilterOrCompositeFilter(Filter filter) {
36+
hardAssert(
37+
filter instanceof FieldFilter || filter instanceof CompositeFilter,
38+
"Only field filters and composite filters are accepted.");
39+
}
40+
41+
/** Returns true if the given filter is a single field filter. e.g. (a == 10). */
42+
private static boolean isSingleFieldFilter(Filter filter) {
43+
return filter instanceof FieldFilter;
44+
}
45+
46+
/**
47+
* Returns true if the given filter is the conjunction of one or more field filters. e.g. (a == 10
48+
* && b == 20)
49+
*/
50+
private static boolean isFlatConjunction(Filter filter) {
51+
return filter instanceof CompositeFilter && ((CompositeFilter) filter).isFlatConjunction();
52+
}
53+
54+
/**
55+
* Returns true if the given filter is the disjunction of one or more "flat conjunctions" and
56+
* field filters. e.g. (a == 10) || (b==20 && c==30)
57+
*/
58+
private static boolean isDisjunctionOfFieldFiltersAndFlatConjunctions(Filter filter) {
59+
if (filter instanceof CompositeFilter) {
60+
CompositeFilter compositeFilter = (CompositeFilter) filter;
61+
if (compositeFilter.isDisjunction()) {
62+
for (Filter subfilter : compositeFilter.getFilters()) {
63+
if (!isSingleFieldFilter(subfilter) && !isFlatConjunction(subfilter)) {
64+
return false;
65+
}
66+
}
67+
return true;
68+
}
69+
}
70+
return false;
71+
}
72+
73+
/**
74+
* Returns whether or not the given filter is in disjunctive normal form (DNF).
75+
*
76+
* <p>In boolean logic, a disjunctive normal form (DNF) is a canonical normal form of a logical
77+
* formula consisting of a disjunction of conjunctions; it can also be described as an OR of ANDs.
78+
*
79+
* <p>For more info, visit: https://en.wikipedia.org/wiki/Disjunctive_normal_form
80+
*/
81+
private static boolean isDisjunctiveNormalForm(Filter filter) {
82+
// A single field filter is always in DNF form.
83+
// An AND of several field filters ("Flat AND") is in DNF form. e.g (A && B).
84+
// An OR of field filters and "Flat AND"s is in DNF form. e.g. A || (B && C) || (D && F).
85+
// Everything else is not in DNF form.
86+
return isSingleFieldFilter(filter)
87+
|| isFlatConjunction(filter)
88+
|| isDisjunctionOfFieldFiltersAndFlatConjunctions(filter);
89+
}
90+
91+
/**
92+
* Applies the associativity property to the given filter and returns the resulting filter.
93+
*
94+
* <ul>
95+
* <li>A | (B | C) == (A | B) | C == (A | B | C)
96+
* <li>A & (B & C) == (A & B) & C == (A & B & C)
97+
* </ul>
98+
*
99+
* <p>For more info, visit: https://en.wikipedia.org/wiki/Associative_property#Propositional_logic
100+
*/
101+
protected static Filter applyAssociation(Filter filter) {
102+
assertFieldFilterOrCompositeFilter(filter);
103+
104+
if (isSingleFieldFilter(filter)) {
105+
return filter;
106+
}
107+
108+
CompositeFilter compositeFilter = (CompositeFilter) filter;
109+
110+
// Example: (A | (((B)) | (C | D) | (E & F & (G | H)) --> (A | B | C | D | (E & F & (G | H))
111+
List<Filter> filters = compositeFilter.getFilters();
112+
113+
// If the composite filter only contains 1 filter, apply associativity to it.
114+
if (filters.size() == 1) {
115+
return applyAssociation(filters.get(0));
116+
}
117+
118+
// Associativity applied to a flat composite filter results in itself.
119+
if (compositeFilter.isFlat()) {
120+
return compositeFilter;
121+
}
122+
123+
// First apply associativity to all subfilters. This will in turn recursively apply
124+
// associativity to all nested composite filters and field filters.
125+
List<Filter> updatedFilters = new ArrayList<>();
126+
for (Filter subfilter : filters) {
127+
updatedFilters.add(applyAssociation(subfilter));
128+
}
129+
130+
// For composite subfilters that perform the same kind of logical operation as `compositeFilter`
131+
// take out their filters and add them to `compositeFilter`. For example:
132+
// compositeFilter = (A | (B | C | D))
133+
// compositeSubfilter = (B | C | D)
134+
// Result: (A | B | C | D)
135+
// Note that the `compositeSubfilter` has been eliminated, and its filters (B, C, D) have been
136+
// added to the top-level "compositeFilter".
137+
List<Filter> newSubfilters = new ArrayList<>();
138+
for (Filter subfilter : updatedFilters) {
139+
if (subfilter instanceof FieldFilter) {
140+
newSubfilters.add(subfilter);
141+
} else if (subfilter instanceof CompositeFilter) {
142+
CompositeFilter compositeSubfilter = (CompositeFilter) subfilter;
143+
if (compositeSubfilter.getOperator().equals(compositeFilter.getOperator())) {
144+
// compositeFilter: (A | (B | C))
145+
// compositeSubfilter: (B | C)
146+
// Result: (A | B | C)
147+
newSubfilters.addAll(compositeSubfilter.getFilters());
148+
} else {
149+
// compositeFilter: (A | (B & C))
150+
// compositeSubfilter: (B & C)
151+
// Result: (A | (B & C))
152+
newSubfilters.add(compositeSubfilter);
153+
}
154+
}
155+
}
156+
if (newSubfilters.size() == 1) {
157+
return newSubfilters.get(0);
158+
}
159+
return new CompositeFilter(newSubfilters, compositeFilter.getOperator());
160+
}
161+
162+
/**
163+
* Performs conjunction distribution for the given filters.
164+
*
165+
* <p>There are generally four types of distributions:
166+
*
167+
* <ul>
168+
* <li>Distribution of conjunction over disjunction: P & (Q | R) == (P & Q) | (P & R)
169+
* <li>Distribution of disjunction over conjunction: P | (Q & R) == (P | Q) & (P | R)
170+
* <li>Distribution of conjunction over conjunction: P & (Q & R) == (P & Q) & (P & R)
171+
* <li>Distribution of disjunction over disjunction: P | (Q | R) == (P | Q) | (P | R)
172+
* </ul>
173+
*
174+
* <p>This function ONLY performs the first type (distributing conjunction over disjunction) as it
175+
* is meant to be used towards arriving at a DNF form.
176+
*
177+
* <p>For more info, visit:
178+
* https://en.wikipedia.org/wiki/Distributive_property#Propositional_logic
179+
*/
180+
protected static Filter applyDistribution(Filter lhs, Filter rhs) {
181+
assertFieldFilterOrCompositeFilter(lhs);
182+
assertFieldFilterOrCompositeFilter(rhs);
183+
Filter result;
184+
if (lhs instanceof FieldFilter && rhs instanceof FieldFilter) {
185+
result = applyDistribution((FieldFilter) lhs, (FieldFilter) rhs);
186+
} else if (lhs instanceof FieldFilter && rhs instanceof CompositeFilter) {
187+
result = applyDistribution((FieldFilter) lhs, (CompositeFilter) rhs);
188+
} else if (lhs instanceof CompositeFilter && rhs instanceof FieldFilter) {
189+
result = applyDistribution((FieldFilter) rhs, (CompositeFilter) lhs);
190+
} else {
191+
result = applyDistribution((CompositeFilter) lhs, (CompositeFilter) rhs);
192+
}
193+
// Since `applyDistribution` is recursive, we must apply association at the end of each
194+
// distribution in order to ensure the result is as flat as possible for the next round of
195+
// distributions.
196+
return applyAssociation(result);
197+
}
198+
199+
private static Filter applyDistribution(FieldFilter lhs, FieldFilter rhs) {
200+
// Conjunction distribution for two field filters is the conjunction of them.
201+
return new CompositeFilter(Arrays.asList(lhs, rhs), Operator.AND);
202+
}
203+
204+
private static Filter applyDistribution(
205+
FieldFilter fieldFilter, CompositeFilter compositeFilter) {
206+
// There are two cases:
207+
// A & (B & C) --> (A & B & C)
208+
// A & (B | C) --> (A & B) | (A & C)
209+
if (compositeFilter.isConjunction()) {
210+
// Case 1
211+
return compositeFilter.withAddedFilters(Collections.singletonList(fieldFilter));
212+
} else {
213+
// Case 2
214+
List<Filter> newFilters = new ArrayList<>();
215+
for (Filter subfilter : compositeFilter.getFilters()) {
216+
newFilters.add(applyDistribution(fieldFilter, subfilter));
217+
}
218+
// TODO(orquery): Use OPERATOR_OR.
219+
return new CompositeFilter(newFilters, Operator.OPERATOR_UNSPECIFIED);
220+
}
221+
}
222+
223+
private static Filter applyDistribution(CompositeFilter lhs, CompositeFilter rhs) {
224+
hardAssert(
225+
!lhs.getFilters().isEmpty() && !rhs.getFilters().isEmpty(),
226+
"Found an empty composite filter");
227+
228+
// There are four cases:
229+
// (A & B) & (C & D) --> (A & B & C & D)
230+
// (A & B) & (C | D) --> (A & B & C) | (A & B & D)
231+
// (A | B) & (C & D) --> (C & D & A) | (C & D & B)
232+
// (A | B) & (C | D) --> (A & C) | (A & D) | (B & C) | (B & D)
233+
234+
// Case 1 is a merge.
235+
if (lhs.isConjunction() && rhs.isConjunction()) {
236+
return lhs.withAddedFilters(rhs.getFilters());
237+
}
238+
239+
// Case 2,3,4 all have at least one side (lhs or rhs) that is a disjunction. In all three cases
240+
// we should take each element of the disjunction and distribute it over the other side, and
241+
// return the disjunction of the distribution results.
242+
CompositeFilter disjunctionSide = lhs.isDisjunction() ? lhs : rhs;
243+
CompositeFilter otherSide = lhs.isDisjunction() ? rhs : lhs;
244+
List<Filter> results = new ArrayList<>();
245+
for (Filter subfilter : disjunctionSide.getFilters()) {
246+
results.add(applyDistribution(subfilter, otherSide));
247+
}
248+
// TODO(orquery): Use OPERATOR_OR.
249+
return new CompositeFilter(results, Operator.OPERATOR_UNSPECIFIED);
250+
}
251+
252+
protected static Filter computeDistributedNormalForm(Filter filter) {
253+
assertFieldFilterOrCompositeFilter(filter);
254+
255+
if (filter instanceof FieldFilter) {
256+
return filter;
257+
}
258+
259+
CompositeFilter compositeFilter = (CompositeFilter) filter;
260+
261+
if (compositeFilter.getFilters().size() == 1) {
262+
return computeDistributedNormalForm(filter.getFilters().get(0));
263+
}
264+
265+
// Compute the DNF for each of the subfilters first.
266+
List<Filter> result = new ArrayList<>();
267+
for (Filter subfilter : compositeFilter.getFilters()) {
268+
result.add(computeDistributedNormalForm(subfilter));
269+
}
270+
Filter newFilter = new CompositeFilter(result, compositeFilter.getOperator());
271+
newFilter = applyAssociation(newFilter);
272+
273+
if (isDisjunctiveNormalForm(newFilter)) {
274+
return newFilter;
275+
}
276+
277+
hardAssert(newFilter instanceof CompositeFilter, "field filters are already in DNF form.");
278+
CompositeFilter newCompositeFilter = (CompositeFilter) newFilter;
279+
hardAssert(
280+
newCompositeFilter.isConjunction(),
281+
"Disjunction of filters all of which are already in DNF form is itself in DNF form.");
282+
hardAssert(
283+
newCompositeFilter.getFilters().size() > 1,
284+
"Single-filter composite filters are already in DNF form.");
285+
Filter runningResult = newCompositeFilter.getFilters().get(0);
286+
for (int i = 1; i < newCompositeFilter.getFilters().size(); ++i) {
287+
runningResult = applyDistribution(runningResult, newCompositeFilter.getFilters().get(i));
288+
}
289+
return runningResult;
290+
}
291+
27292
/**
28293
* Given a composite filter, returns the list of terms in its disjunctive normal form.
29294
*
@@ -35,13 +300,24 @@ public class LogicUtils {
35300
* @param filter the composite filter to calculate DNF transform for.
36301
* @return the terms in the DNF transform.
37302
*/
38-
public static List<Filter> DnfTransform(CompositeFilter filter) {
303+
public static List<Filter> getDnfTerms(CompositeFilter filter) {
39304
// TODO(orquery): write the DNF transform algorithm here.
40305
// For now, assume all inputs are of the form AND(A, B, ...). Therefore the resulting DNF form
41306
// is the same as the input.
42307
if (filter.getFilters().isEmpty()) {
43308
return Collections.emptyList();
44309
}
45-
return Collections.singletonList(filter);
310+
311+
Filter result = computeDistributedNormalForm(filter);
312+
313+
hardAssert(
314+
isDisjunctiveNormalForm(result),
315+
"computeDistributedNormalForm did not result in disjunctive normal form");
316+
317+
if (isSingleFieldFilter(result) || isFlatConjunction(result)) {
318+
return Collections.singletonList(result);
319+
}
320+
321+
return result.getFilters();
46322
}
47323
}

0 commit comments

Comments
 (0)