14
14
15
15
package com .google .firebase .firestore .util ;
16
16
17
+ import static com .google .firebase .firestore .util .Assert .hardAssert ;
18
+
17
19
import com .google .firebase .firestore .core .CompositeFilter ;
20
+ import com .google .firebase .firestore .core .FieldFilter ;
18
21
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 ;
19
25
import java .util .Collections ;
20
26
import java .util .List ;
21
27
24
30
* complex filters used in queries.
25
31
*/
26
32
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
+
27
292
/**
28
293
* Given a composite filter, returns the list of terms in its disjunctive normal form.
29
294
*
@@ -35,13 +300,24 @@ public class LogicUtils {
35
300
* @param filter the composite filter to calculate DNF transform for.
36
301
* @return the terms in the DNF transform.
37
302
*/
38
- public static List <Filter > DnfTransform (CompositeFilter filter ) {
303
+ public static List <Filter > getDnfTerms (CompositeFilter filter ) {
39
304
// TODO(orquery): write the DNF transform algorithm here.
40
305
// For now, assume all inputs are of the form AND(A, B, ...). Therefore the resulting DNF form
41
306
// is the same as the input.
42
307
if (filter .getFilters ().isEmpty ()) {
43
308
return Collections .emptyList ();
44
309
}
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 ();
46
322
}
47
323
}
0 commit comments