]> git.cworth.org Git - notmuch/commitdiff
lib/parse-sexp: support and, not, and or.
authorDavid Bremner <david@tethera.net>
Tue, 24 Aug 2021 15:17:19 +0000 (08:17 -0700)
committerDavid Bremner <david@tethera.net>
Sun, 5 Sep 2021 00:07:19 +0000 (17:07 -0700)
All operations and (Xapian) fields will eventually have an entry in
the prefixes table. The flags field is just a placeholder for now, but
will eventually distinguish between various kinds of prefixes.

doc/man7/notmuch-sexp-queries.rst
lib/parse-sexp.cc
test/T081-sexpr-search.sh

index d177934d97df075b961976287b4a113aaf9989a1..0304759e2418b68b09ed7491404ebb75c084c835 100644 (file)
@@ -51,7 +51,9 @@ subqueries.
     (for most fields) or *or*. See :any:`fields` for more information.
 
 ``(`` *operator* |q1| |q2| ... |qn| ``)``
     (for most fields) or *or*. See :any:`fields` for more information.
 
 ``(`` *operator* |q1| |q2| ... |qn| ``)``
-    Combine queries |q1| to |qn|. See :any:`operators` for more information.
+    Combine queries |q1| to |qn|. Currently supported operators are
+    ``and``, ``or``, and ``not``. ``(not`` |q1| ... |qn| ``)`` is equivalent
+    to ``(and (not`` |q1| ``) ... (not`` |qn| ``))``.
 
 ``(`` *modifier* |q1| |q2| ... |qn| ``)``
     Combine queries |q1| to |qn|, and reinterpret the result (e.g. as a regular expression).
 
 ``(`` *modifier* |q1| |q2| ... |qn| ``)``
     Combine queries |q1| to |qn|, and reinterpret the result (e.g. as a regular expression).
@@ -62,11 +64,6 @@ subqueries.
 FIELDS
 ``````
 
 FIELDS
 ``````
 
-.. _operators:
-
-OPERATORS
-`````````
-
 .. _modifiers:
 
 MODIFIERS
 .. _modifiers:
 
 MODIFIERS
@@ -82,6 +79,13 @@ EXAMPLES
     Match all messages containing "added", but also those containing "add", "additional",
     "Additional", "adds", etc... via stemming.
 
     Match all messages containing "added", but also those containing "add", "additional",
     "Additional", "adds", etc... via stemming.
 
+``(and Bob Marley)``
+    Match messages containing words "Bob" and "Marley", or their stems
+    The words need not be adjacent.
+
+``(not Bob Marley)``
+    Match messages containing neither "Bob" nor "Marley", nor their stems,
+
 .. |q1| replace:: :math:`q_1`
 .. |q2| replace:: :math:`q_2`
 .. |qn| replace:: :math:`q_n`
 .. |q1| replace:: :math:`q_1`
 .. |q2| replace:: :math:`q_2`
 .. |qn| replace:: :math:`q_n`
index f031d7904a4bf8246979c819882736b9346f36ed..0d2c0ba85e2cabee764ff0fba04151c3fd5efd22 100644 (file)
@@ -7,12 +7,69 @@
 /* _sexp is used for file scope symbols to avoid clashing with
  * definitions from sexp.h */
 
 /* _sexp is used for file scope symbols to avoid clashing with
  * definitions from sexp.h */
 
+typedef enum {
+    SEXP_FLAG_NONE = 0,
+} _sexp_flag_t;
+
+typedef struct  {
+    const char *name;
+    Xapian::Query::op xapian_op;
+    Xapian::Query initial;
+    _sexp_flag_t flags;
+} _sexp_prefix_t;
+
+static _sexp_prefix_t prefixes[] =
+{
+    { "and",            Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_NONE },
+    { "not",            Xapian::Query::OP_AND_NOT,      Xapian::Query::MatchAll,
+      SEXP_FLAG_NONE },
+    { "or",             Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_NONE },
+    { }
+};
+
+static notmuch_status_t _sexp_to_xapian_query (notmuch_database_t *notmuch,
+                                              const _sexp_prefix_t *parent,
+                                              const sexp_t *sx,
+                                              Xapian::Query &output);
+
+static notmuch_status_t
+_sexp_combine_query (notmuch_database_t *notmuch,
+                    const _sexp_prefix_t *parent,
+                    Xapian::Query::op operation,
+                    Xapian::Query left,
+                    const sexp_t *sx,
+                    Xapian::Query &output)
+{
+    Xapian::Query subquery;
+
+    notmuch_status_t status;
+
+    /* if we run out elements, return accumulator */
+
+    if (! sx) {
+       output = left;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    status = _sexp_to_xapian_query (notmuch, parent, sx, subquery);
+    if (status)
+       return status;
+
+    return _sexp_combine_query (notmuch,
+                               parent,
+                               operation,
+                               Xapian::Query (operation, left, subquery),
+                               sx->next, output);
+}
+
 /* Here we expect the s-expression to be a proper list, with first
  * element defining and operation, or as a special case the empty
  * list */
 
 static notmuch_status_t
 /* Here we expect the s-expression to be a proper list, with first
  * element defining and operation, or as a special case the empty
  * list */
 
 static notmuch_status_t
-_sexp_to_xapian_query (notmuch_database_t *notmuch, const sexp_t *sx,
+_sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, const sexp_t *sx,
                       Xapian::Query &output)
 {
 
                       Xapian::Query &output)
 {
 
@@ -32,11 +89,20 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const sexp_t *sx,
        return NOTMUCH_STATUS_SUCCESS;
     }
 
        return NOTMUCH_STATUS_SUCCESS;
     }
 
-    if (sx->list->ty == SEXP_VALUE)
-       _notmuch_database_log (notmuch, "unknown prefix '%s'\n", sx->list->val);
-    else
+    if (sx->list->ty == SEXP_LIST) {
        _notmuch_database_log (notmuch, "unexpected list in field/operation position\n",
                               sx->list->val);
        _notmuch_database_log (notmuch, "unexpected list in field/operation position\n",
                               sx->list->val);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    for (_sexp_prefix_t *prefix = prefixes; prefix && prefix->name; prefix++) {
+       if (strcmp (prefix->name, sx->list->val) == 0) {
+           return _sexp_combine_query (notmuch, parent, prefix->xapian_op, prefix->initial,
+                                       sx->list->next, output);
+       }
+    }
+
+    _notmuch_database_log (notmuch, "unknown prefix '%s'\n", sx->list->val);
 
     return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
 }
 
     return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
 }
@@ -54,6 +120,6 @@ _notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *q
        return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
     }
 
        return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
     }
 
-    return _sexp_to_xapian_query (notmuch, sx, output);
+    return _sexp_to_xapian_query (notmuch, NULL, sx, output);
 }
 #endif
 }
 #endif
index 4e3d755c60f7fb4836dec6539f121a3a09c44e47..5e1bb18d0471e18e3f6fe2d884b7a3c11d5b846d 100755 (executable)
@@ -9,9 +9,34 @@ fi
 
 add_email_corpus
 
 
 add_email_corpus
 
-test_begin_subtest "all messages: ()"
-notmuch search '*' > EXPECTED
-notmuch search --query=sexp "()" > OUTPUT
+for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \
+            '(or (and) (or) (not (and)))'; do
+    test_begin_subtest "all messages: $query"
+    notmuch search '*' > EXPECTED
+    notmuch search --query=sexp "$query" > OUTPUT
+    test_expect_equal_file EXPECTED OUTPUT
+done
+
+for query in '(or)' '(not ())' '(not (not))' '(not (and))' \
+                   '(not (or (and) (or) (not (and))))'; do
+    test_begin_subtest "no messages: $query"
+    notmuch search --query=sexp "$query" > OUTPUT
+    test_expect_equal_file /dev/null OUTPUT
+done
+
+test_begin_subtest "and of exact terms"
+notmuch search --query=sexp '(and "wonderful" "wizard")' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "or of exact terms"
+notmuch search --query=sexp '(or "php" "wizard")' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2010-12-29 [1/1] François Boulogne; [aur-general] Guidelines: cp, mkdir vs install (inbox unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+EOF
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "single term in body"
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "single term in body"