2 * @brief Query-related tests.
4 /* Copyright (C) 2008,2009,2012,2013,2015,2016,2017,2019 Olly Betts
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of the
9 * License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
24 #include "api_query.h"
28 #include "testsuite.h"
29 #include "testutils.h"
35 DEFINE_TESTCASE(queryterms1
, !backend
) {
36 Xapian::Query query
= Xapian::Query::MatchAll
;
37 /// Regression test - in 1.0.10 and earlier "" was included in the list.
38 TEST(query
.get_terms_begin() == query
.get_terms_end());
39 TEST(query
.get_unique_terms_begin() == query
.get_unique_terms_end());
40 query
= Xapian::Query(query
.OP_AND_NOT
, query
, Xapian::Query("fair"));
41 TEST_EQUAL(*query
.get_terms_begin(), "fair");
42 TEST_EQUAL(*query
.get_unique_terms_begin(), "fair");
44 Xapian::QueryParser qp
;
45 Xapian::Query q
= qp
.parse_query("\"the the the\"");
47 auto t
= q
.get_terms_begin();
49 while (t
!= q
.get_terms_end()) {
50 TEST_EQUAL(*t
, "the");
57 auto t
= q
.get_unique_terms_begin();
59 while (t
!= q
.get_unique_terms_end()) {
60 TEST_EQUAL(*t
, "the");
68 DEFINE_TESTCASE(matchall2
, !backend
) {
69 TEST_STRINGS_EQUAL(Xapian::Query::MatchAll
.get_description(),
70 "Query(<alldocuments>)");
73 DEFINE_TESTCASE(matchnothing1
, !backend
) {
74 TEST_STRINGS_EQUAL(Xapian::Query::MatchNothing
.get_description(),
76 vector
<Xapian::Query
> subqs
;
77 subqs
.push_back(Xapian::Query("foo"));
78 subqs
.push_back(Xapian::Query::MatchNothing
);
79 Xapian::Query
q(Xapian::Query::OP_AND
, subqs
.begin(), subqs
.end());
80 TEST_STRINGS_EQUAL(q
.get_description(), "Query()");
82 Xapian::Query
q2(Xapian::Query::OP_AND
,
83 Xapian::Query("foo"), Xapian::Query::MatchNothing
);
84 TEST_STRINGS_EQUAL(q2
.get_description(), "Query()");
86 Xapian::Query
q3(Xapian::Query::OP_AND
,
87 Xapian::Query::MatchNothing
, Xapian::Query("foo"));
88 TEST_STRINGS_EQUAL(q2
.get_description(), "Query()");
90 Xapian::Query
q4(Xapian::Query::OP_AND_MAYBE
,
91 Xapian::Query("foo"), Xapian::Query::MatchNothing
);
92 TEST_STRINGS_EQUAL(q4
.get_description(), "Query(foo)");
94 Xapian::Query
q5(Xapian::Query::OP_AND_MAYBE
,
95 Xapian::Query::MatchNothing
, Xapian::Query("foo"));
96 TEST_STRINGS_EQUAL(q5
.get_description(), "Query()");
98 Xapian::Query
q6(Xapian::Query::OP_AND_NOT
,
99 Xapian::Query("foo"), Xapian::Query::MatchNothing
);
100 TEST_STRINGS_EQUAL(q6
.get_description(), "Query(foo)");
102 Xapian::Query
q7(Xapian::Query::OP_AND_NOT
,
103 Xapian::Query::MatchNothing
, Xapian::Query("foo"));
104 TEST_STRINGS_EQUAL(q7
.get_description(), "Query()");
107 DEFINE_TESTCASE(overload1
, !backend
) {
109 q
= Xapian::Query("foo") & Xapian::Query("bar");
110 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo AND bar))");
112 // Test &= appends a same-type subquery (since Xapian 1.4.10).
113 q
&= Xapian::Query("baz");
114 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo AND bar AND baz))");
115 // But not if the RHS is the same query:
116 q
= Xapian::Query("foo") & Xapian::Query("bar");
118 # if __has_warning("-Wself-assign-overloaded")
119 // Suppress warning from newer clang about self-assignment so we can
120 // test that self-assignment works!
121 # pragma clang diagnostic push
122 # pragma clang diagnostic ignored "-Wself-assign-overloaded"
127 # if __has_warning("-Wself-assign-overloaded")
128 # pragma clang diagnostic pop
131 TEST_STRINGS_EQUAL(q
.get_description(), "Query(((foo AND bar) AND (foo AND bar)))");
133 // Also not if the query has a refcount > 1.
134 q
= Xapian::Query("foo") & Xapian::Query("bar");
135 Xapian::Query qcopy
= q
;
136 qcopy
&= Xapian::Query("baz");
137 TEST_STRINGS_EQUAL(qcopy
.get_description(), "Query(((foo AND bar) AND baz))");
138 // And q shouldn't change.
139 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo AND bar))");
141 // Check that MatchNothing still results in MatchNothing:
142 q
= Xapian::Query("foo") & Xapian::Query("bar");
143 q
&= Xapian::Query::MatchNothing
;
144 TEST_STRINGS_EQUAL(q
.get_description(), "Query()");
145 // Check we don't combine for other operators:
146 q
= Xapian::Query("foo") | Xapian::Query("bar");
147 q
&= Xapian::Query("baz");
148 TEST_STRINGS_EQUAL(q
.get_description(), "Query(((foo OR bar) AND baz))");
150 // Test |= appends a same-type subquery (since Xapian 1.4.10).
151 q
= Xapian::Query("foo") | Xapian::Query("bar");
152 q
|= Xapian::Query("baz");
153 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo OR bar OR baz))");
154 // But not if the RHS is the same query:
155 q
= Xapian::Query("foo") | Xapian::Query("bar");
157 # if __has_warning("-Wself-assign-overloaded")
158 // Suppress warning from newer clang about self-assignment so we can
159 // test that self-assignment works!
160 # pragma clang diagnostic push
161 # pragma clang diagnostic ignored "-Wself-assign-overloaded"
166 # if __has_warning("-Wself-assign-overloaded")
167 # pragma clang diagnostic pop
170 TEST_STRINGS_EQUAL(q
.get_description(), "Query(((foo OR bar) OR (foo OR bar)))");
172 // Also not if the query has a refcount > 1.
173 q
= Xapian::Query("foo") | Xapian::Query("bar");
174 Xapian::Query qcopy
= q
;
175 qcopy
|= Xapian::Query("baz");
176 TEST_STRINGS_EQUAL(qcopy
.get_description(), "Query(((foo OR bar) OR baz))");
177 // And q shouldn't change.
178 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo OR bar))");
180 // Check that MatchNothing still results in no change:
181 q
= Xapian::Query("foo") | Xapian::Query("bar");
182 q
|= Xapian::Query::MatchNothing
;
183 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo OR bar))");
184 // Check we don't combine for other operators:
185 q
= Xapian::Query("foo") & Xapian::Query("bar");
186 q
|= Xapian::Query("baz");
187 TEST_STRINGS_EQUAL(q
.get_description(), "Query(((foo AND bar) OR baz))");
189 // Test ^= appends a same-type subquery (since Xapian 1.4.10).
190 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
191 q
^= Xapian::Query("baz");
192 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo XOR bar XOR baz))");
193 // But a query ^= itself gives an empty query.
194 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
196 # if __has_warning("-Wself-assign-overloaded")
197 // Suppress warning from newer clang about self-assignment so we can
198 // test that self-assignment works!
199 # pragma clang diagnostic push
200 # pragma clang diagnostic ignored "-Wself-assign-overloaded"
205 # if __has_warning("-Wself-assign-overloaded")
206 # pragma clang diagnostic pop
209 TEST_STRINGS_EQUAL(q
.get_description(), "Query()");
211 // Even if the reference count > 1.
212 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
213 Xapian::Query qcopy
= q
;
215 TEST_STRINGS_EQUAL(q
.get_description(), "Query()");
218 // Also not if the query has a refcount > 1.
219 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
220 Xapian::Query qcopy
= q
;
221 qcopy
^= Xapian::Query("baz");
222 TEST_STRINGS_EQUAL(qcopy
.get_description(), "Query(((foo XOR bar) XOR baz))");
223 // And q shouldn't change.
224 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo XOR bar))");
226 // Check that MatchNothing still results in no change:
227 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
228 q
^= Xapian::Query::MatchNothing
;
229 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo XOR bar))");
230 // Check we don't combine for other operators:
231 q
= Xapian::Query("foo") & Xapian::Query("bar");
232 q
^= Xapian::Query("baz");
233 TEST_STRINGS_EQUAL(q
.get_description(), "Query(((foo AND bar) XOR baz))");
235 q
= Xapian::Query("foo") &~ Xapian::Query("bar");
236 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo AND_NOT bar))");
237 // In 1.4.9 and earlier this gave (foo AND (<alldocuments> AND_NOT bar)).
238 q
= Xapian::Query("foo");
239 q
&= ~Xapian::Query("bar");
240 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo AND_NOT bar))");
241 q
= ~Xapian::Query("bar");
242 TEST_STRINGS_EQUAL(q
.get_description(), "Query((<alldocuments> AND_NOT bar))");
243 q
= Xapian::Query("foo") & Xapian::Query::MatchNothing
;
244 TEST_STRINGS_EQUAL(q
.get_description(), "Query()");
245 q
= Xapian::Query("foo") | Xapian::Query("bar");
246 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo OR bar))");
247 q
= Xapian::Query("foo") | Xapian::Query::MatchNothing
;
248 TEST_STRINGS_EQUAL(q
.get_description(), "Query(foo)");
249 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
250 TEST_STRINGS_EQUAL(q
.get_description(), "Query((foo XOR bar))");
251 q
= Xapian::Query("foo") ^ Xapian::Query::MatchNothing
;
252 TEST_STRINGS_EQUAL(q
.get_description(), "Query(foo)");
253 q
= 1.25 * (Xapian::Query("one") | Xapian::Query("two"));
254 TEST_STRINGS_EQUAL(q
.get_description(), "Query(1.25 * (one OR two))");
255 q
= (Xapian::Query("one") & Xapian::Query("two")) * 42;
256 TEST_STRINGS_EQUAL(q
.get_description(), "Query(42 * (one AND two))");
257 q
= Xapian::Query("one") / 2.0;
258 TEST_STRINGS_EQUAL(q
.get_description(), "Query(0.5 * one)");
261 /** Regression test and feature test.
263 * This threw AssertionError in 1.0.9 and earlier (bug#201) and gave valgrind
264 * errors in 1.0.11 and earlier (bug#349).
266 * Currently the OR-subquery case is supported, other operators aren't.
268 DEFINE_TESTCASE(possubqueries1
, writable
) {
269 Xapian::WritableDatabase db
= get_writable_database();
270 Xapian::Document doc
;
271 doc
.add_posting("a", 1);
272 doc
.add_posting("b", 2);
273 doc
.add_posting("c", 3);
274 db
.add_document(doc
);
276 Xapian::Query
a_or_b(Xapian::Query::OP_OR
,
279 Xapian::Query
near(Xapian::Query::OP_NEAR
, a_or_b
, a_or_b
);
280 // As of 1.3.0, we no longer rearrange queries at this point, so check
282 TEST_STRINGS_EQUAL(near
.get_description(),
283 "Query(((a OR b) NEAR 2 (a OR b)))");
284 Xapian::Query
phrase(Xapian::Query::OP_PHRASE
, a_or_b
, a_or_b
);
285 TEST_STRINGS_EQUAL(phrase
.get_description(),
286 "Query(((a OR b) PHRASE 2 (a OR b)))");
288 Xapian::Query
a_and_b(Xapian::Query::OP_AND
,
291 Xapian::Query
a_near_b(Xapian::Query::OP_NEAR
,
294 Xapian::Query
a_phrs_b(Xapian::Query::OP_PHRASE
,
297 Xapian::Query
c("c");
299 // FIXME: The plan is to actually try to support the cases below, but
300 // for now at least ensure they are cleanly rejected.
301 Xapian::Enquire
enq(db
);
303 TEST_EXCEPTION(Xapian::UnimplementedError
,
304 Xapian::Query
q(Xapian::Query::OP_NEAR
, a_and_b
, c
);
306 (void)enq
.get_mset(0, 10));
308 TEST_EXCEPTION(Xapian::UnimplementedError
,
309 Xapian::Query
q(Xapian::Query::OP_NEAR
, a_near_b
, c
);
311 (void)enq
.get_mset(0, 10));
313 TEST_EXCEPTION(Xapian::UnimplementedError
,
314 Xapian::Query
q(Xapian::Query::OP_NEAR
, a_phrs_b
, c
);
316 (void)enq
.get_mset(0, 10));
318 TEST_EXCEPTION(Xapian::UnimplementedError
,
319 Xapian::Query
q(Xapian::Query::OP_PHRASE
, a_and_b
, c
);
321 (void)enq
.get_mset(0, 10));
323 TEST_EXCEPTION(Xapian::UnimplementedError
,
324 Xapian::Query
q(Xapian::Query::OP_PHRASE
, a_near_b
, c
);
326 (void)enq
.get_mset(0, 10));
328 TEST_EXCEPTION(Xapian::UnimplementedError
,
329 Xapian::Query
q(Xapian::Query::OP_PHRASE
, a_phrs_b
, c
);
331 (void)enq
.get_mset(0, 10));
334 /// Test that XOR handles all remaining subqueries running out at the same
336 DEFINE_TESTCASE(xor3
, backend
) {
337 Xapian::Database db
= get_database("apitest_simpledata");
339 static const char * const subqs
[] = {
340 "hack", "which", "paragraph", "is", "return"
342 // Document where the subqueries run out *does* match XOR:
343 Xapian::Query
q(Xapian::Query::OP_XOR
, subqs
, subqs
+ 5);
344 Xapian::Enquire
enq(db
);
346 Xapian::MSet mset
= enq
.get_mset(0, 10);
348 TEST_EQUAL(mset
.size(), 3);
349 TEST_EQUAL(*mset
[0], 4);
350 TEST_EQUAL(*mset
[1], 2);
351 TEST_EQUAL(*mset
[2], 3);
353 // Document where the subqueries run out *does not* match XOR:
354 q
= Xapian::Query(Xapian::Query::OP_XOR
, subqs
, subqs
+ 4);
356 mset
= enq
.get_mset(0, 10);
358 TEST_EQUAL(mset
.size(), 4);
359 TEST_EQUAL(*mset
[0], 5);
360 TEST_EQUAL(*mset
[1], 4);
361 TEST_EQUAL(*mset
[2], 2);
362 TEST_EQUAL(*mset
[3], 3);
365 /// Check encoding of non-UTF8 terms in query descriptions.
366 DEFINE_TESTCASE(nonutf8termdesc1
, !backend
) {
367 TEST_EQUAL(Xapian::Query("\xc0\x80\xf5\x80\x80\x80\xfe\xff").get_description(),
368 "Query(\\xc0\\x80\\xf5\\x80\\x80\\x80\\xfe\\xff)");
369 TEST_EQUAL(Xapian::Query(string("\x00\x1f", 2)).get_description(),
370 "Query(\\x00\\x1f)");
371 // Check that backslashes are encoded so output isn't ambiguous.
372 TEST_EQUAL(Xapian::Query("back\\slash").get_description(),
373 "Query(back\\x5cslash)");
374 // Check that \x7f is escaped.
375 TEST_EQUAL(Xapian::Query("D\x7f_\x7f~").get_description(),
376 "Query(D\\x7f_\\x7f~)");
379 /// Test introspection on Query objects.
380 DEFINE_TESTCASE(queryintro1
, !backend
) {
381 TEST_EQUAL(Xapian::Query::MatchAll
.get_type(), Xapian::Query::LEAF_MATCH_ALL
);
382 TEST_EQUAL(Xapian::Query::MatchAll
.get_num_subqueries(), 0);
383 TEST_EQUAL(Xapian::Query::MatchNothing
.get_type(), Xapian::Query::LEAF_MATCH_NOTHING
);
384 TEST_EQUAL(Xapian::Query::MatchNothing
.get_num_subqueries(), 0);
387 q
= Xapian::Query(q
.OP_AND_NOT
, Xapian::Query::MatchAll
, Xapian::Query("fair"));
388 TEST_EQUAL(q
.get_type(), q
.OP_AND_NOT
);
389 TEST_EQUAL(q
.get_num_subqueries(), 2);
390 TEST_EQUAL(q
.get_subquery(0).get_type(), q
.LEAF_MATCH_ALL
);
391 TEST_EQUAL(q
.get_subquery(1).get_type(), q
.LEAF_TERM
);
393 q
= Xapian::Query("foo") & Xapian::Query("bar");
394 TEST_EQUAL(q
.get_type(), q
.OP_AND
);
396 q
= Xapian::Query("foo") &~ Xapian::Query("bar");
397 TEST_EQUAL(q
.get_type(), q
.OP_AND_NOT
);
399 q
= ~Xapian::Query("bar");
400 TEST_EQUAL(q
.get_type(), q
.OP_AND_NOT
);
402 q
= Xapian::Query("foo") | Xapian::Query("bar");
403 TEST_EQUAL(q
.get_type(), q
.OP_OR
);
405 q
= Xapian::Query("foo") ^ Xapian::Query("bar");
406 TEST_EQUAL(q
.get_type(), q
.OP_XOR
);
408 q
= 1.25 * (Xapian::Query("one") | Xapian::Query("two"));
409 TEST_EQUAL(q
.get_type(), q
.OP_SCALE_WEIGHT
);
410 TEST_EQUAL(q
.get_num_subqueries(), 1);
411 TEST_EQUAL(q
.get_subquery(0).get_type(), q
.OP_OR
);
413 q
= Xapian::Query("one") / 2.0;
414 TEST_EQUAL(q
.get_type(), q
.OP_SCALE_WEIGHT
);
415 TEST_EQUAL(q
.get_num_subqueries(), 1);
416 TEST_EQUAL(q
.get_subquery(0).get_type(), q
.LEAF_TERM
);
418 q
= Xapian::Query(q
.OP_NEAR
, Xapian::Query("a"), Xapian::Query("b"));
419 TEST_EQUAL(q
.get_type(), q
.OP_NEAR
);
420 TEST_EQUAL(q
.get_num_subqueries(), 2);
421 TEST_EQUAL(q
.get_subquery(0).get_type(), q
.LEAF_TERM
);
422 TEST_EQUAL(q
.get_subquery(1).get_type(), q
.LEAF_TERM
);
424 q
= Xapian::Query(q
.OP_PHRASE
, Xapian::Query("c"), Xapian::Query("d"));
425 TEST_EQUAL(q
.get_type(), q
.OP_PHRASE
);
426 TEST_EQUAL(q
.get_num_subqueries(), 2);
427 TEST_EQUAL(q
.get_subquery(0).get_type(), q
.LEAF_TERM
);
428 TEST_EQUAL(q
.get_subquery(1).get_type(), q
.LEAF_TERM
);
431 /// Regression test for bug introduced in 1.3.1 and fixed in 1.3.3.
432 // We were incorrectly converting a term which indexed all docs and was used
433 // in an unweighted phrase into an all docs postlist, so check that this
434 // case actually works.
435 DEFINE_TESTCASE(phrasealldocs1
, backend
) {
436 Xapian::Database db
= get_database("apitest_declen");
438 static const char * const phrase
[] = { "this", "is", "the" };
439 q
= Xapian::Query(q
.OP_AND_NOT
,
440 Xapian::Query("paragraph"),
441 Xapian::Query(q
.OP_PHRASE
, phrase
, phrase
+ 3));
442 Xapian::Enquire
enq(db
);
444 Xapian::MSet mset
= enq
.get_mset(0, 10);
445 TEST_EQUAL(mset
.size(), 3);
448 struct wildcard_testcase
{
449 const char * pattern
;
450 Xapian::termcount max_expansion
;
452 const char * terms
[4];
455 #define WILDCARD_EXCEPTION { 0, 0, 0, "" }
457 wildcard_testcase wildcard1_testcases
[] = {
458 // Tries to expand to 7 terms.
459 { "th", 6, 'E', WILDCARD_EXCEPTION
},
460 { "thou", 1, 'E', { "though", 0, 0, 0 } },
461 { "s", 2, 'F', { "say", "search", 0, 0 } },
462 { "s", 2, 'M', { "simpl", "so", 0, 0 } }
465 DEFINE_TESTCASE(wildcard1
, backend
) {
466 // FIXME: The counting of terms the wildcard expands to is per subdatabase,
467 // so the wildcard may expand to more terms than the limit if some aren't
468 // in all subdatabases. Also WILDCARD_LIMIT_MOST_FREQUENT uses the
469 // frequency from the subdatabase, and so may select different terms in
471 SKIP_TEST_FOR_BACKEND("multi");
472 Xapian::Database db
= get_database("apitest_simpledata");
473 Xapian::Enquire
enq(db
);
474 const Xapian::Query::op o
= Xapian::Query::OP_WILDCARD
;
476 for (auto&& test
: wildcard1_testcases
) {
477 tout
<< test
.pattern
<< endl
;
478 auto tend
= test
.terms
+ 4;
479 while (tend
[-1] == NULL
) --tend
;
480 bool expect_exception
= (tend
- test
.terms
== 4 && tend
[-1][0] == '\0');
484 switch (test
.max_type
) {
486 max_type
= Xapian::Query::WILDCARD_LIMIT_ERROR
;
489 max_type
= Xapian::Query::WILDCARD_LIMIT_FIRST
;
492 max_type
= Xapian::Query::WILDCARD_LIMIT_MOST_FREQUENT
;
495 FAIL_TEST("Unexpected max_type value");
497 q
= Xapian::Query(o
, test
.pattern
, test
.max_expansion
, max_type
);
499 q
= Xapian::Query(o
, test
.pattern
, test
.max_expansion
);
503 Xapian::MSet mset
= enq
.get_mset(0, 10);
504 TEST(!expect_exception
);
505 q
= Xapian::Query(q
.OP_SYNONYM
, test
.terms
, tend
);
507 Xapian::MSet mset2
= enq
.get_mset(0, 10);
508 TEST_EQUAL(mset
.size(), mset2
.size());
509 TEST(mset_range_is_same(mset
, 0, mset2
, 0, mset
.size()));
510 } catch (const Xapian::WildcardError
&) {
511 TEST(expect_exception
);
516 /// Regression test for #696, fixed in 1.3.4.
517 DEFINE_TESTCASE(wildcard2
, backend
) {
518 // FIXME: The counting of terms the wildcard expands to is per subdatabase,
519 // so the wildcard may expand to more terms than the limit if some aren't
520 // in all subdatabases. Also WILDCARD_LIMIT_MOST_FREQUENT uses the
521 // frequency from the subdatabase, and so may select different terms in
523 SKIP_TEST_FOR_BACKEND("multi");
524 Xapian::Database db
= get_database("apitest_simpledata");
525 Xapian::Enquire
enq(db
);
526 const Xapian::Query::op o
= Xapian::Query::OP_WILDCARD
;
528 const int max_type
= Xapian::Query::WILDCARD_LIMIT_MOST_FREQUENT
;
529 Xapian::Query
q0(o
, "w", 2, max_type
);
530 Xapian::Query
q(o
, "s", 2, max_type
);
531 Xapian::Query
q2(o
, "t", 2, max_type
);
532 q
= Xapian::Query(q
.OP_OR
, q0
, q
);
533 q
= Xapian::Query(q
.OP_OR
, q
, q2
);
535 Xapian::MSet mset
= enq
.get_mset(0, 10);
536 TEST_EQUAL(mset
.size(), 6);
539 DEFINE_TESTCASE(dualprefixwildcard1
, backend
) {
540 Xapian::Database db
= get_database("apitest_simpledata");
541 Xapian::Query
q(Xapian::Query::OP_SYNONYM
,
542 Xapian::Query(Xapian::Query::OP_WILDCARD
, "fo"),
543 Xapian::Query(Xapian::Query::OP_WILDCARD
, "Sfo"));
544 tout
<< q
.get_description() << endl
;
545 Xapian::Enquire
enq(db
);
547 TEST_EQUAL(enq
.get_mset(0, 5).size(), 2);
550 struct positional_testcase
{
552 const char * terms
[4];
553 Xapian::docid result
;
557 positional_testcase loosephrase1_testcases
[] = {
558 { 5, { "expect", "to", "mset", 0 }, 0 },
559 { 5, { "word", "well", "the", 0 }, 2 },
560 { 5, { "if", "word", "doesnt", 0 }, 0 },
561 { 5, { "at", "line", "three", 0 }, 0 },
562 { 5, { "paragraph", "other", "the", 0 }, 0 },
563 { 5, { "other", "the", "with", 0 }, 0 }
566 /// Regression test for bug fixed in 1.3.3 and 1.2.21.
567 DEFINE_TESTCASE(loosephrase1
, backend
) {
568 Xapian::Database db
= get_database("apitest_simpledata");
569 Xapian::Enquire
enq(db
);
571 for (auto&& test
: loosephrase1_testcases
) {
572 auto tend
= test
.terms
+ 4;
573 while (tend
[-1] == NULL
) --tend
;
574 auto OP_PHRASE
= Xapian::Query::OP_PHRASE
;
575 Xapian::Query
q(OP_PHRASE
, test
.terms
, tend
, test
.window
);
577 Xapian::MSet mset
= enq
.get_mset(0, 10);
578 if (test
.result
== 0) {
581 TEST_EQUAL(mset
.size(), 1);
582 TEST_EQUAL(*mset
[0], test
.result
);
588 positional_testcase loosenear1_testcases
[] = {
589 { 4, { "test", "the", "with", 0 }, 1 },
590 { 4, { "expect", "word", "the", 0 }, 2 },
591 { 4, { "line", "be", "blank", 0 }, 1 },
592 { 2, { "banana", "banana", 0, 0 }, 0 },
593 { 3, { "banana", "banana", 0, 0 }, 0 },
594 { 2, { "word", "word", 0, 0 }, 2 },
595 { 4, { "work", "meant", "work", 0 }, 0 },
596 { 4, { "this", "one", "yet", "one" }, 0 }
599 /// Regression tests for bugs fixed in 1.3.3 and 1.2.21.
600 DEFINE_TESTCASE(loosenear1
, backend
) {
601 Xapian::Database db
= get_database("apitest_simpledata");
602 Xapian::Enquire
enq(db
);
604 for (auto&& test
: loosenear1_testcases
) {
605 auto tend
= test
.terms
+ 4;
606 while (tend
[-1] == NULL
) --tend
;
607 Xapian::Query
q(Xapian::Query::OP_NEAR
, test
.terms
, tend
, test
.window
);
609 Xapian::MSet mset
= enq
.get_mset(0, 10);
610 if (test
.result
== 0) {
613 TEST_EQUAL(mset
.size(), 1);
614 TEST_EQUAL(*mset
[0], test
.result
);
619 /// Regression test for bug fixed in 1.3.6 - the first case segfaulted in 1.3.x.
620 DEFINE_TESTCASE(complexphrase1
, backend
) {
621 Xapian::Database db
= get_database("apitest_simpledata");
622 Xapian::Enquire
enq(db
);
623 Xapian::Query
query(Xapian::Query::OP_PHRASE
,
624 Xapian::Query("a") | Xapian::Query("b"),
626 enq
.set_query(query
);
627 TEST(enq
.get_mset(0, 10).empty());
628 Xapian::Query
query2(Xapian::Query::OP_PHRASE
,
629 Xapian::Query("a") | Xapian::Query("b"),
631 enq
.set_query(query2
);
632 TEST(enq
.get_mset(0, 10).empty());
635 /// Regression test for bug fixed in 1.3.6 - the first case segfaulted in 1.3.x.
636 DEFINE_TESTCASE(complexnear1
, backend
) {
637 Xapian::Database db
= get_database("apitest_simpledata");
638 Xapian::Enquire
enq(db
);
639 Xapian::Query
query(Xapian::Query::OP_NEAR
,
640 Xapian::Query("a") | Xapian::Query("b"),
642 enq
.set_query(query
);
643 TEST(enq
.get_mset(0, 10).empty());
644 Xapian::Query
query2(Xapian::Query::OP_NEAR
,
645 Xapian::Query("a") | Xapian::Query("b"),
647 enq
.set_query(query2
);
648 TEST(enq
.get_mset(0, 10).empty());
651 /// Check subqueries of MatchAll, MatchNothing and PostingSource are supported.
652 DEFINE_TESTCASE(complexphrase2
, backend
) {
653 Xapian::Database db
= get_database("apitest_simpledata");
654 Xapian::Enquire
enq(db
);
655 Xapian::ValueWeightPostingSource
ps(0);
656 Xapian::Query subqs
[3] = {
657 Xapian::Query(Xapian::Query::OP_PHRASE
,
660 Xapian::Query(Xapian::Query::OP_PHRASE
,
661 Xapian::Query("and"),
662 Xapian::Query::MatchAll
),
663 Xapian::Query(Xapian::Query::OP_PHRASE
,
665 Xapian::Query::MatchNothing
)
667 Xapian::Query
query(Xapian::Query::OP_OR
, subqs
, subqs
+ 3);
668 enq
.set_query(query
);
669 (void)enq
.get_mset(0, 10);
672 /// Check subqueries of MatchAll, MatchNothing and PostingSource are supported.
673 DEFINE_TESTCASE(complexnear2
, backend
) {
674 Xapian::Database db
= get_database("apitest_simpledata");
675 Xapian::Enquire
enq(db
);
676 Xapian::ValueWeightPostingSource
ps(0);
677 Xapian::Query subqs
[3] = {
678 Xapian::Query(Xapian::Query::OP_NEAR
,
681 Xapian::Query(Xapian::Query::OP_NEAR
,
682 Xapian::Query("and"),
683 Xapian::Query::MatchAll
),
684 Xapian::Query(Xapian::Query::OP_NEAR
,
686 Xapian::Query::MatchNothing
)
688 Xapian::Query
query(Xapian::Query::OP_OR
, subqs
, subqs
+ 3);
689 enq
.set_query(query
);
690 (void)enq
.get_mset(0, 10);
693 /// A zero estimated number of matches broke the code to round the estimate.
694 DEFINE_TESTCASE(zeroestimate1
, backend
) {
695 Xapian::Enquire
enquire(get_database("apitest_simpledata"));
696 Xapian::Query
phrase(Xapian::Query::OP_PHRASE
,
697 Xapian::Query("absolute"),
698 Xapian::Query("rubbish"));
699 enquire
.set_query(phrase
&~ Xapian::Query("queri"));
700 Xapian::MSet mset
= enquire
.get_mset(0, 0);
701 TEST_EQUAL(mset
.get_matches_estimated(), 0);
704 /// Feature test for OR under OP_PHRASE support added in 1.4.3.
705 DEFINE_TESTCASE(complexphrase3
, backend
) {
706 Xapian::Database db
= get_database("apitest_simpledata");
707 Xapian::Enquire
enq(db
);
708 Xapian::Query
query(Xapian::Query::OP_PHRASE
,
709 Xapian::Query("is") | Xapian::Query("as") | Xapian::Query("be"),
711 enq
.set_query(query
);
712 mset_expect_order(enq
.get_mset(0, 10), 1);
713 Xapian::Query
query2(Xapian::Query::OP_PHRASE
,
715 Xapian::Query("is") | Xapian::Query("as") | Xapian::Query("be"));
716 enq
.set_query(query2
);
717 mset_expect_order(enq
.get_mset(0, 10));
718 Xapian::Query
query3(Xapian::Query::OP_PHRASE
,
719 Xapian::Query("one") | Xapian::Query("with"),
720 Xapian::Query("the") | Xapian::Query("of") | Xapian::Query("line"));
721 enq
.set_query(query3
);
722 mset_expect_order(enq
.get_mset(0, 10), 1, 4, 5);
723 Xapian::Query
query4(Xapian::Query::OP_PHRASE
,
724 Xapian::Query("the") | Xapian::Query("of") | Xapian::Query("line"),
725 Xapian::Query("one") | Xapian::Query("with"));
726 enq
.set_query(query4
);
727 mset_expect_order(enq
.get_mset(0, 10));
730 /// Feature test for OR under OP_NEAR support added in 1.4.3.
731 DEFINE_TESTCASE(complexnear3
, backend
) {
732 Xapian::Database db
= get_database("apitest_simpledata");
733 Xapian::Enquire
enq(db
);
734 Xapian::Query
query(Xapian::Query::OP_NEAR
,
735 Xapian::Query("is") | Xapian::Query("as") | Xapian::Query("be"),
737 enq
.set_query(query
);
738 mset_expect_order(enq
.get_mset(0, 10), 1);
739 Xapian::Query
query2(Xapian::Query::OP_NEAR
,
741 Xapian::Query("is") | Xapian::Query("as") | Xapian::Query("be"));
742 enq
.set_query(query2
);
743 mset_expect_order(enq
.get_mset(0, 10), 1);
744 Xapian::Query
query3(Xapian::Query::OP_NEAR
,
745 Xapian::Query("one") | Xapian::Query("with"),
746 Xapian::Query("the") | Xapian::Query("of") | Xapian::Query("line"));
747 enq
.set_query(query3
);
748 mset_expect_order(enq
.get_mset(0, 10), 1, 4, 5);
749 Xapian::Query
query4(Xapian::Query::OP_NEAR
,
750 Xapian::Query("the") | Xapian::Query("of") | Xapian::Query("line"),
751 Xapian::Query("one") | Xapian::Query("with"));
752 enq
.set_query(query4
);
753 mset_expect_order(enq
.get_mset(0, 10), 1, 4, 5);
757 gen_subdbwithoutpos1_db(Xapian::WritableDatabase
& db
, const string
&)
759 Xapian::Document doc
;
760 doc
.add_term("this");
761 doc
.add_term("paragraph");
762 doc
.add_term("wibble", 5);
763 db
.add_document(doc
);
766 DEFINE_TESTCASE(subdbwithoutpos1
, generated
) {
767 XFAIL_FOR_BACKEND("remote",
768 "Known but obscure remote bug which doesn't justify "
769 "protocol version bump");
770 XFAIL_FOR_BACKEND("multi_remote",
771 "Known but obscure remote bug which doesn't justify "
772 "protocol version bump");
774 Xapian::Database
db(get_database("apitest_simpledata"));
776 Xapian::Query
q(Xapian::Query::OP_PHRASE
,
777 Xapian::Query("this"),
778 Xapian::Query("paragraph"));
780 Xapian::Enquire
enq1(db
);
782 Xapian::MSet mset1
= enq1
.get_mset(0, 10);
783 TEST_EQUAL(mset1
.size(), 3);
785 Xapian::Database db2
=
786 get_database("subdbwithoutpos1", gen_subdbwithoutpos1_db
);
788 // If a database has no positional info, OP_PHRASE -> OP_AND.
789 Xapian::Enquire
enq2(db2
);
791 Xapian::MSet mset2
= enq2
.get_mset(0, 10);
792 TEST_EQUAL(mset2
.size(), 1);
794 // If one sub-database in a combined database has no positional info but
795 // other sub-databases do, then we shouldn't convert OP_PHRASE to OP_AND
796 // (but prior to 1.4.3 we did).
797 db
.add_database(db2
);
798 Xapian::Enquire
enq3(db
);
800 Xapian::MSet mset3
= enq3
.get_mset(0, 10);
801 TEST_EQUAL(mset3
.size(), 3);
802 // Regression test for bug introduced in 1.4.3 which led to a division by
803 // zero and then (at least on Linux) we got 1% here.
804 TEST_EQUAL(mset3
[0].get_percent(), 100);
806 // Regression test for https://trac.xapian.org/ticket/752
807 enq3
.set_query((Xapian::Query("this") & q
) | Xapian::Query("wibble"));
808 mset3
= enq3
.get_mset(0, 10);
809 TEST_EQUAL(mset3
.size(), 4);
812 // Regression test for bug fixed in 1.4.4 and 1.2.25.
813 DEFINE_TESTCASE(notandor1
, backend
) {
814 Xapian::Database
db(get_database("etext"));
816 Query q
= Query("the") &~ (Query("friedrich") &
817 (Query("day") | Query("night")));
818 Xapian::Enquire
enq(db
);
821 Xapian::MSet mset
= enq
.get_mset(0, 10, db
.get_doccount());
822 TEST_EQUAL(mset
.get_matches_estimated(), 344);
825 // Regression test for bug fixed in git master before 1.5.0.
826 DEFINE_TESTCASE(boolorbug1
, backend
) {
827 Xapian::Database
db(get_database("etext"));
829 Query q
= Query("the") &~ Query(Query::OP_WILDCARD
, "pru");
830 Xapian::Enquire
enq(db
);
833 Xapian::MSet mset
= enq
.get_mset(0, 10, db
.get_doccount());
834 // Due to a bug in BoolOrPostList this returned 330 results.
835 TEST_EQUAL(mset
.get_matches_estimated(), 331);
838 // Regression test for bug introduced in 1.4.13 and fixed in 1.4.14.
839 DEFINE_TESTCASE(hoistnotbug1
, backend
) {
840 Xapian::Database
db(get_database("etext"));
842 Query
q(Query::OP_PHRASE
, Query("the"), Query("king"));
843 q
&= ~Query("worldtornado");
845 Xapian::Enquire
enq(db
);
848 // This reliably fails before the fix in an assertion build, and may crash
850 Xapian::MSet mset
= enq
.get_mset(0, 10, db
.get_doccount());
851 TEST_EQUAL(mset
.get_matches_estimated(), 42);
854 // Regression test for segfault optimising query on git master before 1.5.0.
855 DEFINE_TESTCASE(emptynot1
, backend
) {
856 Xapian::Database
db(get_database("apitest_simpledata"));
857 Xapian::Enquire
enq(db
);
858 enq
.set_weighting_scheme(Xapian::BoolWeight());
859 Xapian::Query query
= Xapian::Query("document") & Xapian::Query("api");
860 // This range won't match anything, so collapses to MatchNothing as we
861 // optimise the query.
862 query
= Xapian::Query(query
.OP_AND_NOT
,
864 Xapian::Query(Xapian::Query::OP_VALUE_GE
, 1234, "x"));
865 enq
.set_query(query
);
866 Xapian::MSet mset
= enq
.get_mset(0, 10);
867 TEST_EQUAL(mset
.size(), 1);
868 // Essentially the same test but with a term which doesn't match anything
869 // instead of a range.
870 query
= Xapian::Query("document") & Xapian::Query("api");
871 query
= Xapian::Query(query
.OP_AND_NOT
,
873 Xapian::Query("nosuchterm"));
874 enq
.set_query(query
);
875 mset
= enq
.get_mset(0, 10);
876 TEST_EQUAL(mset
.size(), 1);
879 // Similar case to emptynot1 but for OP_AND_MAYBE. This case wasn't failing,
880 // so this isn't a regression test, but we do want to ensure it works.
881 DEFINE_TESTCASE(emptymaybe1
, backend
) {
882 Xapian::Database
db(get_database("apitest_simpledata"));
883 Xapian::Enquire
enq(db
);
884 enq
.set_weighting_scheme(Xapian::BoolWeight());
885 Xapian::Query query
= Xapian::Query("document") & Xapian::Query("api");
886 // This range won't match anything, so collapses to MatchNothing as we
887 // optimise the query.
888 query
= Xapian::Query(query
.OP_AND_MAYBE
,
890 Xapian::Query(Xapian::Query::OP_VALUE_GE
, 1234, "x"));
891 enq
.set_query(query
);
892 Xapian::MSet mset
= enq
.get_mset(0, 10);
893 TEST_EQUAL(mset
.size(), 1);
894 // Essentially the same test but with a term which doesn't match anything
895 // instead of a range.
896 query
= Xapian::Query("document") & Xapian::Query("api");
897 query
= Xapian::Query(query
.OP_AND_MAYBE
,
899 Xapian::Query("nosuchterm"));
900 enq
.set_query(query
);
901 mset
= enq
.get_mset(0, 10);
902 TEST_EQUAL(mset
.size(), 1);
905 DEFINE_TESTCASE(phraseweightcheckbug1
, backend
) {
906 Xapian::Database
db(get_database("phraseweightcheckbug1"));
907 Xapian::Enquire
enq(db
);
908 static const char* const words
[] = {"hello", "world"};
909 Xapian::Query query
{Xapian::Query::OP_PHRASE
, begin(words
), end(words
), 2};
910 query
= Xapian::Query(query
.OP_OR
, query
, Xapian::Query("most"));
911 tout
<< query
.get_description() << '\n';
912 enq
.set_query(query
);
913 Xapian::MSet mset
= enq
.get_mset(0, 3);
914 TEST_EQUAL(mset
.size(), 3);
917 DEFINE_TESTCASE(orphanedhint1
, backend
) {
918 Xapian::Database
db(get_database("apitest_simpledata"));
919 Xapian::Enquire
enq(db
);
920 auto OP_WILDCARD
= Xapian::Query::OP_WILDCARD
;
921 Xapian::Query query
= Xapian::Query(OP_WILDCARD
, "doc") &
922 Xapian::Query(OP_WILDCARD
, "xyzzy");
923 query
|= Xapian::Query("test");
924 tout
<< query
.get_description() << '\n';
925 enq
.set_query(query
);
926 Xapian::MSet mset
= enq
.get_mset(0, 3);
927 TEST_EQUAL(mset
.size(), 1);