Ticket #857: 0001-Fix-AndNotPostList-skip_to-before-it-has-been-positi.patch

File 0001-Fix-AndNotPostList-skip_to-before-it-has-been-positi.patch, 6.3 KB (added by Adrian Ratiu, 8 hours ago)

claude-fix.patch

  • xapian-core/matcher/andnotpostlist.cc

    From 368a5e2d2a62be9ff59c8bdbe5961c33a98ac203 Mon Sep 17 00:00:00 2001
    From: Adrian Ratiu <adrian.ratiu@collabora.com>
    Date: Fri, 30 Oct 2026 15:44:26 +0200
    Subject: [PATCH] Fix AndNotPostList::skip_to() before it has been positioned
    
    skip_to() did nothing if the target docid wasn't after the left side's
    current docid, but before the first next() or skip_to() that current
    docid is unspecified, and a leaf postlist may already return its first
    docid then.  So the first skip_to() could stop on that docid without
    checking it against the right side, and return a document which the
    AND_NOT should exclude.
    
    It took BoolWeight to hit this in practice: the OR is then a
    BoolOrPostList, which passes skip_to() straight to its subqueries, so
    in ((A AND_NOT B) OR C) AND D, where D has the lower termfreq, the
    AND_NOT is first skip_to()'d to D's first docid.  If that's the first
    docid of A and it also has B, it was wrongly matched.
    
    notmuch uses BoolWeight for every search and count, so its searches
    which OR "x and not y" with another query and AND the result could
    return mail that "and not y" excludes.
    
    Always forward skip_to() to the left side, which already does nothing
    if it is at or past the target, and check the result against the right
    side.  1.4 tracked the left side's position itself and wasn't affected;
    this regressed when AndNotPostList was reimplemented using
    WrapperPostList.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    ---
     xapian-core/matcher/andnotpostlist.cc | 58 ++++++++++++++-------------
     xapian-core/tests/api_query.cc        | 38 ++++++++++++++++++
     2 files changed, 68 insertions(+), 28 deletions(-)
    
    diff --git a/xapian-core/matcher/andnotpostlist.cc b/xapian-core/matcher/andnotpostlist.cc
    index cfc94af4834c..7c35e897f2e8 100644
    a b AndNotPostList::next(double w_min)  
    6666PostList*
    6767AndNotPostList::skip_to(Xapian::docid did, double w_min)
    6868{
    69     if (did > pl->get_docid()) {
    70         PostList* result = pl->skip_to(did, w_min);
     69    // We can't skip the work here when did <= pl->get_docid(): before we've
     70    // been positioned, get_docid() is unspecified, and a leaf postlist may
     71    // already return its first docid, which r hasn't yet been checked
     72    // against.  pl->skip_to() does nothing if pl is already at or past did.
     73    PostList* result = pl->skip_to(did, w_min);
     74    if (result) {
     75        delete pl;
     76        pl = result;
     77    }
     78    if (pl->at_end()) {
     79        result = pl;
     80        pl = nullptr;
     81        return result;
     82    }
     83    Xapian::docid l_did = pl->get_docid();
     84    if (l_did > r_did) {
     85        bool r_valid;
     86        result = r->check(l_did, 0, r_valid);
    7187        if (result) {
    72             delete pl;
    73             pl = result;
     88            delete r;
     89            r = result;
    7490        }
    75         if (pl->at_end()) {
     91        if (!r_valid)
     92            return nullptr;
     93        if (r->at_end()) {
    7694            result = pl;
    77             pl = NULL;
     95            pl = nullptr;
    7896            return result;
    7997        }
    80         Xapian::docid l_did = pl->get_docid();
    81         if (l_did > r_did) {
    82             bool r_valid;
    83             result = r->check(l_did, 0, r_valid);
    84             if (result) {
    85                 delete r;
    86                 r = result;
    87             }
    88             if (!r_valid)
    89                 return NULL;
    90             if (r->at_end()) {
    91                 result = pl;
    92                 pl = NULL;
    93                 return result;
    94             }
    95             r_did = r->get_docid();
    96         }
    97         if (l_did == r_did) {
    98             // Advance to the next match.
    99             return AndNotPostList::next(w_min);
    100         }
     98        r_did = r->get_docid();
    10199    }
    102     return NULL;
     100    if (l_did == r_did) {
     101        // Advance to the next match.
     102        return AndNotPostList::next(w_min);
     103    }
     104    return nullptr;
    103105}
    104106
    105107PostList*
  • xapian-core/tests/api_query.cc

    diff --git a/xapian-core/tests/api_query.cc b/xapian-core/tests/api_query.cc
    index 4780a354d9cf..0fbb9a6fc6cb 100644
    a b DEFINE_TESTCASE(estimateopbug2, backend && !remote) {  
    14951495    } catch (const Exception_estimateop2&) {
    14961496    }
    14971497}
     1498
     1499/// Regression test for bug in AndNotPostList::skip_to() fixed after 2.1.0.
     1500DEFINE_TESTCASE(boolandnotbug1, backend) {
     1501    Xapian::Database db = get_database("boolandnotbug1",
     1502                                       [](Xapian::WritableDatabase& wdb,
     1503                                          const string&)
     1504                                       {
     1505                                           Xapian::Document doc;
     1506                                           doc.add_term("A");
     1507                                           doc.add_term("B");
     1508                                           doc.add_term("D");
     1509                                           wdb.add_document(doc);
     1510                                           Xapian::Document doc2;
     1511                                           doc2.add_term("C");
     1512                                           wdb.add_document(doc2);
     1513                                           Xapian::Document doc3;
     1514                                           doc3.add_term("A");
     1515                                           doc3.add_term("D");
     1516                                           wdb.add_document(doc3);
     1517                                           // More docs matching just C, so
     1518                                           // that D has the lower termfreq,
     1519                                           // the AND advances it first and
     1520                                           // calls skip_to() on the OR.
     1521                                           wdb.add_document(doc2);
     1522                                           wdb.add_document(doc2);
     1523                                       });
     1524    Xapian::Query q = (Xapian::Query("A") &~ Xapian::Query("B")) |
     1525                      Xapian::Query("C");
     1526    q &= Xapian::Query("D");
     1527    Xapian::Enquire enq(db);
     1528    enq.set_query(q);
     1529    mset_expect_order(enq.get_mset(0, 10), 3);
     1530    // With BoolWeight the OR is a BoolOrPostList, which calls skip_to() on
     1531    // the AND_NOT before it has been positioned, and the AND_NOT used to
     1532    // return docid 1 without checking it against the right side.
     1533    enq.set_weighting_scheme(Xapian::BoolWeight());
     1534    mset_expect_order(enq.get_mset(0, 10), 3);
     1535}