Add: Query coalescing for AND clauses
A later commit will update and add tests.
This commit is contained in:
parent
0057972069
commit
968a249ae3
3 changed files with 123 additions and 32 deletions
|
|
@ -544,6 +544,9 @@ Simple links may also be written manually in either sexp or non-sexp form, like:
|
||||||
*Changed*
|
*Changed*
|
||||||
+ Give more useful error message for invalid queries.
|
+ Give more useful error message for invalid queries.
|
||||||
|
|
||||||
|
*Internal*
|
||||||
|
+ Certain query predicates, when called multiple times in an ~and~ sub-expression, are optimized to a single call.
|
||||||
|
|
||||||
** 0.6.2
|
** 0.6.2
|
||||||
|
|
||||||
*Fixed*
|
*Fixed*
|
||||||
|
|
|
||||||
92
org-ql.el
92
org-ql.el
|
|
@ -694,6 +694,56 @@ returns nil."
|
||||||
;; partially typed queries in the Helm commands.
|
;; partially typed queries in the Helm commands.
|
||||||
(define-error 'org-ql-invalid-query "Invalid Org QL query" 'user-error)
|
(define-error 'org-ql-invalid-query "Invalid Org QL query" 'user-error)
|
||||||
|
|
||||||
|
(defun org-ql--coalesce-ands (query)
|
||||||
|
"Return QUERY having coalesced any AND'ed clauses' predicates.
|
||||||
|
Multiple calls to the same predicate within an `and' expression
|
||||||
|
are coalesced into a single call to the predicate.
|
||||||
|
|
||||||
|
Note that this is a relatively simple function which does not
|
||||||
|
comprehensively coalesce every call that could be. For example,
|
||||||
|
if QUERY contained four calls to the `src' predicate with two
|
||||||
|
unique language arguments, only the calls for one language would
|
||||||
|
be coalesced."
|
||||||
|
;; TODO: Use a per-predicate alist-getting function that accounts
|
||||||
|
;; for arguments which must be unique...maybe...someday...
|
||||||
|
|
||||||
|
;; NOTE: This implentation can sometimes reorder sub-expressions,
|
||||||
|
;; like:
|
||||||
|
;;
|
||||||
|
;; (and (src :regexps ("foo") :lang "elisp") (src :regexps ("bar")))
|
||||||
|
;;
|
||||||
|
;; becomes:
|
||||||
|
;;
|
||||||
|
;; (and (src :regexps ("bar")) (src :regexps ("foo") :lang "elisp"))
|
||||||
|
;;
|
||||||
|
;; because the first one could be coalescable, but the second one
|
||||||
|
;; can't be coalesced with it since they don't specify the same
|
||||||
|
;; language. That could be fixed, but it's probably not worth it.
|
||||||
|
(cl-labels ((rec (sexp)
|
||||||
|
(pcase sexp
|
||||||
|
(`(,(and boolean (or 'or 'not)) . ,sexps)
|
||||||
|
`(,boolean ,@(mapcar #'rec sexps)))
|
||||||
|
(`(and . ,sexps)
|
||||||
|
(anded sexps))
|
||||||
|
(_ sexp)))
|
||||||
|
(anded (sexps)
|
||||||
|
(let (anded-predicates new-sexp)
|
||||||
|
(dolist (sexp sexps)
|
||||||
|
(pcase sexp
|
||||||
|
(`(,(or 'or 'not) . ,_)
|
||||||
|
(push (rec sexp) new-sexp))
|
||||||
|
(`(,predicate . ,args)
|
||||||
|
(pcase-exhaustive (plist-get (alist-get predicate org-ql-predicates) :coalesce-multiple-calls)
|
||||||
|
(`nil (push sexp new-sexp))
|
||||||
|
(`t (setf (alist-get predicate anded-predicates)
|
||||||
|
(append (alist-get predicate anded-predicates) args)))
|
||||||
|
((and fn (pred functionp))
|
||||||
|
(if-let (new-args (funcall fn (alist-get predicate anded-predicates) args))
|
||||||
|
(setf (alist-get predicate anded-predicates) new-args)
|
||||||
|
(push sexp new-sexp)))))))
|
||||||
|
(delq nil `(and ,@(nreverse new-sexp) ,@(nreverse anded-predicates))))))
|
||||||
|
(rec query)))
|
||||||
|
|
||||||
(defun org-ql--sanity-check-form (form)
|
(defun org-ql--sanity-check-form (form)
|
||||||
"Signal error if any forms in FORM do not have preconditions met.
|
"Signal error if any forms in FORM do not have preconditions met.
|
||||||
Or, when possible, fix the problem."
|
Or, when possible, fix the problem."
|
||||||
|
|
@ -967,7 +1017,7 @@ defined in `org-ql-predicates' by calling `org-ql-defpred'."
|
||||||
(setf query new-query)
|
(setf query new-query)
|
||||||
(when (eq (cl-incf count) limit)
|
(when (eq (cl-incf count) limit)
|
||||||
(error "Query normalization limit exceeded: QUERY:%S" query)))
|
(error "Query normalization limit exceeded: QUERY:%S" query)))
|
||||||
finally return new-query)))))))
|
finally return (org-ql--coalesce-ands new-query))))))))
|
||||||
|
|
||||||
(defun org-ql--define-query-preamble-fn (predicates)
|
(defun org-ql--define-query-preamble-fn (predicates)
|
||||||
"Define function `org-ql--query-preamble' for PREDICATES.
|
"Define function `org-ql--query-preamble' for PREDICATES.
|
||||||
|
|
@ -1037,7 +1087,7 @@ defined in `org-ql-predicates' by calling `org-ql-defpred'."
|
||||||
;; function still works. But to avoid the warning, we byte-compile it afterward.
|
;; function still works. But to avoid the warning, we byte-compile it afterward.
|
||||||
(byte-compile 'org-ql--query-preamble)))
|
(byte-compile 'org-ql--query-preamble)))
|
||||||
|
|
||||||
(cl-defmacro org-ql-defpred (name args docstring &key body preambles normalizers)
|
(cl-defmacro org-ql-defpred (name args docstring &key body preambles normalizers coalesce-multiple-calls)
|
||||||
"Define an `org-ql' selector predicate named `org-ql--predicate-NAME'.
|
"Define an `org-ql' selector predicate named `org-ql--predicate-NAME'.
|
||||||
NAME may be a symbol or a list of symbols: if a list, the first
|
NAME may be a symbol or a list of symbols: if a list, the first
|
||||||
is used as NAME and the rest are aliases. A function is only
|
is used as NAME and the rest are aliases. A function is only
|
||||||
|
|
@ -1091,6 +1141,22 @@ to variables bound in the pattern:
|
||||||
query expression with no work to do, which improves
|
query expression with no work to do, which improves
|
||||||
performance.
|
performance.
|
||||||
|
|
||||||
|
When COALESCE-MULTIPLE-CALLS is t, multiple calls to this
|
||||||
|
predicate within a query clause may be combined into a single
|
||||||
|
call to this predicate (so it is expected that this predicate
|
||||||
|
treats multiple arguments as being boolean AND'ed together).
|
||||||
|
|
||||||
|
This value may also be a function called to do coalescing of two
|
||||||
|
predicate expressions. It is called with two arguments: the list
|
||||||
|
of already-coalesced arguments to an expression, and the list of
|
||||||
|
arguments to the call being coalesced (note that a query's
|
||||||
|
arguments are normalized before the query is coalesced). If it
|
||||||
|
returns nil, the expression is not coalesced; otherwise, it
|
||||||
|
should return a new list of arguments coalescing the given
|
||||||
|
arguments, with new arguments being first. (This is useful,
|
||||||
|
e.g. when a predicate takes keyword arguments, which means that
|
||||||
|
multiple calls to it can't be simply appended.)
|
||||||
|
|
||||||
For convenience, within the `pcase' patterns, the symbol
|
For convenience, within the `pcase' patterns, the symbol
|
||||||
`predicate-names' is a special form which is replaced with a
|
`predicate-names' is a special form which is replaced with a
|
||||||
pattern matching any of the predicate's name and aliases. For
|
pattern matching any of the predicate's name and aliases. For
|
||||||
|
|
@ -1107,6 +1173,8 @@ It would be expanded to:
|
||||||
|
|
||||||
((`(,(or 'heading 'h) . ,args)
|
((`(,(or 'heading 'h) . ,args)
|
||||||
`(heading ,@args)))"
|
`(heading ,@args)))"
|
||||||
|
;; FIXME: Update defpred tutorial to include :coalesce-multiple-calls.
|
||||||
|
|
||||||
;; NOTE: The debug form works, completely! For example, use `edebug-defun'
|
;; NOTE: The debug form works, completely! For example, use `edebug-defun'
|
||||||
;; on the `heading' predicate, then evaluate this form:
|
;; on the `heading' predicate, then evaluate this form:
|
||||||
;; (let* ((query '(heading "HEADING"))
|
;; (let* ((query '(heading "HEADING"))
|
||||||
|
|
@ -1116,7 +1184,8 @@ It would be expanded to:
|
||||||
;; :normalized normalized
|
;; :normalized normalized
|
||||||
;; :preamble preamble))
|
;; :preamble preamble))
|
||||||
(declare (debug ([&or symbolp listp] listp stringp
|
(declare (debug ([&or symbolp listp] listp stringp
|
||||||
&rest [&or [":body" def-body]
|
&rest [&or [":coalesce-multiple-calls" form]
|
||||||
|
[":body" def-body]
|
||||||
[":normalizers" (&rest (sexp def-body))]
|
[":normalizers" (&rest (sexp def-body))]
|
||||||
[":preambles" (&rest (sexp def-body))]]))
|
[":preambles" (&rest (sexp def-body))]]))
|
||||||
(indent defun))
|
(indent defun))
|
||||||
|
|
@ -1137,7 +1206,8 @@ It would be expanded to:
|
||||||
;; SOMEDAY: Use `map-elt' here, after map 2.1 can be automatically installed in CI sandbox...
|
;; SOMEDAY: Use `map-elt' here, after map 2.1 can be automatically installed in CI sandbox...
|
||||||
(setf (alist-get ',predicate-name org-ql-predicates)
|
(setf (alist-get ',predicate-name org-ql-predicates)
|
||||||
`(:name ,',name :aliases ,',aliases :fn ,',fn-name :docstring ,(\, docstring) :args ,',args
|
`(:name ,',name :aliases ,',aliases :fn ,',fn-name :docstring ,(\, docstring) :args ,',args
|
||||||
:normalizers ,',normalizers :preambles ,',preambles))
|
:normalizers ,',normalizers :preambles ,',preambles
|
||||||
|
:coalesce-multiple-calls ,,coalesce-multiple-calls))
|
||||||
(unless org-ql-defpred-defer
|
(unless org-ql-defpred-defer
|
||||||
;; Reversing preserves the order in which predicates were defined.
|
;; Reversing preserves the order in which predicates were defined.
|
||||||
(org-ql--define-normalize-query-fn (reverse org-ql-predicates))
|
(org-ql--define-normalize-query-fn (reverse org-ql-predicates))
|
||||||
|
|
@ -1320,6 +1390,7 @@ Org effort string, like \"5\" or \"0:05\"."
|
||||||
(org-ql-defpred (heading h) (&rest strings)
|
(org-ql-defpred (heading h) (&rest strings)
|
||||||
"Return non-nil if current entry's heading matches all STRINGS.
|
"Return non-nil if current entry's heading matches all STRINGS.
|
||||||
Matching is done case-insensitively."
|
Matching is done case-insensitively."
|
||||||
|
:coalesce-multiple-calls t
|
||||||
:normalizers ((`(,predicate-names . ,args)
|
:normalizers ((`(,predicate-names . ,args)
|
||||||
;; "h" alias.
|
;; "h" alias.
|
||||||
`(heading ,@args)))
|
`(heading ,@args)))
|
||||||
|
|
@ -1346,6 +1417,7 @@ Matching is done case-insensitively."
|
||||||
(org-ql-defpred (heading-regexp h*) (&rest regexps)
|
(org-ql-defpred (heading-regexp h*) (&rest regexps)
|
||||||
"Return non-nil if current entry's heading matches all REGEXPS (regexp strings).
|
"Return non-nil if current entry's heading matches all REGEXPS (regexp strings).
|
||||||
Matching is done case-insensitively."
|
Matching is done case-insensitively."
|
||||||
|
:coalesce-multiple-calls t
|
||||||
:normalizers ((`(,predicate-names . ,args)
|
:normalizers ((`(,predicate-names . ,args)
|
||||||
;; "h" alias.
|
;; "h" alias.
|
||||||
`(heading-regexp ,@args)))
|
`(heading-regexp ,@args)))
|
||||||
|
|
@ -1498,6 +1570,7 @@ the following queries:
|
||||||
(olp \"Food\" \"Fruits\")
|
(olp \"Food\" \"Fruits\")
|
||||||
(olp \"Fruits\" \"Grapes\")
|
(olp \"Fruits\" \"Grapes\")
|
||||||
(olp \"Food\" \"Grapes\")"
|
(olp \"Food\" \"Grapes\")"
|
||||||
|
:coalesce-multiple-calls t
|
||||||
:normalizers ((`(,predicate-names . ,strings)
|
:normalizers ((`(,predicate-names . ,strings)
|
||||||
;; Regexp quote headings.
|
;; Regexp quote headings.
|
||||||
`(org-ql--predicate-outline-path ,@(mapcar #'regexp-quote strings))))
|
`(org-ql--predicate-outline-path ,@(mapcar #'regexp-quote strings))))
|
||||||
|
|
@ -1652,6 +1725,7 @@ priority B)."
|
||||||
|
|
||||||
(org-ql-defpred (regexp r) (&rest regexps)
|
(org-ql-defpred (regexp r) (&rest regexps)
|
||||||
"Return non-nil if current entry matches all of REGEXPS (regexp strings)."
|
"Return non-nil if current entry matches all of REGEXPS (regexp strings)."
|
||||||
|
:coalesce-multiple-calls t
|
||||||
:normalizers ((`(,predicate-names . ,args)
|
:normalizers ((`(,predicate-names . ,args)
|
||||||
`(regexp ,@args)))
|
`(regexp ,@args)))
|
||||||
;; MAYBE: Separate case-sensitive (Regexp) predicate.
|
;; MAYBE: Separate case-sensitive (Regexp) predicate.
|
||||||
|
|
@ -1674,6 +1748,15 @@ priority B)."
|
||||||
"Return non-nil if current entry contains an Org source block matching all of REGEXPS.
|
"Return non-nil if current entry contains an Org source block matching all of REGEXPS.
|
||||||
If keyword argument LANG is non-nil, the block must be in that
|
If keyword argument LANG is non-nil, the block must be in that
|
||||||
language."
|
language."
|
||||||
|
:coalesce-multiple-calls (lambda (coalesced-args current-args)
|
||||||
|
(when (or (not coalesced-args)
|
||||||
|
(equal (plist-get current-args :lang)
|
||||||
|
(plist-get coalesced-args :lang)))
|
||||||
|
(setf (plist-get coalesced-args :regexps)
|
||||||
|
(append (plist-get coalesced-args :regexps)
|
||||||
|
(plist-get current-args :regexps))
|
||||||
|
(plist-get coalesced-args :lang) (plist-get current-args :lang))
|
||||||
|
coalesced-args))
|
||||||
:normalizers ((`(,predicate-names . ,args)
|
:normalizers ((`(,predicate-names . ,args)
|
||||||
;; Rewrite to use keyword args.
|
;; Rewrite to use keyword args.
|
||||||
(cond ((cl-every #'stringp args)
|
(cond ((cl-every #'stringp args)
|
||||||
|
|
@ -1739,6 +1822,7 @@ Tests both inherited and local tags."
|
||||||
(org-ql-defpred (tags-all tags&) (&rest tags)
|
(org-ql-defpred (tags-all tags&) (&rest tags)
|
||||||
"Return non-nil if current heading has all of TAGS (a list of strings).
|
"Return non-nil if current heading has all of TAGS (a list of strings).
|
||||||
Tests both inherited and local tags."
|
Tests both inherited and local tags."
|
||||||
|
:coalesce-multiple-calls t
|
||||||
;; MAYBE: -all versions for inherited and local.
|
;; MAYBE: -all versions for inherited and local.
|
||||||
:normalizers ((`(,predicate-names . ,tags)
|
:normalizers ((`(,predicate-names . ,tags)
|
||||||
`(and ,@(--map `(tags ,it) tags))))
|
`(and ,@(--map `(tags ,it) tags))))
|
||||||
|
|
|
||||||
60
org-ql.info
60
org-ql.info
|
|
@ -1015,6 +1015,10 @@ File: README.info, Node: 07-pre, Next: 062, Up: Changelog
|
||||||
*Changed*
|
*Changed*
|
||||||
• Give more useful error message for invalid queries.
|
• Give more useful error message for invalid queries.
|
||||||
|
|
||||||
|
*Internal*
|
||||||
|
• Certain query predicates, when called multiple times in an ‘and’
|
||||||
|
sub-expression, are optimized to a single call.
|
||||||
|
|
||||||
|
|
||||||
File: README.info, Node: 062, Next: 061, Prev: 07-pre, Up: Changelog
|
File: README.info, Node: 062, Next: 061, Prev: 07-pre, Up: Changelog
|
||||||
|
|
||||||
|
|
@ -1659,34 +1663,34 @@ Node: Links36258
|
||||||
Node: Tips36945
|
Node: Tips36945
|
||||||
Node: Changelog37269
|
Node: Changelog37269
|
||||||
Node: 07-pre38037
|
Node: 07-pre38037
|
||||||
Node: 06238642
|
Node: 06238786
|
||||||
Node: 06138950
|
Node: 06139094
|
||||||
Node: 0639518
|
Node: 0639662
|
||||||
Node: 05242572
|
Node: 05242716
|
||||||
Node: 05142872
|
Node: 05143016
|
||||||
Node: 0543295
|
Node: 0543439
|
||||||
Node: 04944824
|
Node: 04944968
|
||||||
Node: 04845104
|
Node: 04845248
|
||||||
Node: 04745451
|
Node: 04745595
|
||||||
Node: 04645860
|
Node: 04646004
|
||||||
Node: 04546268
|
Node: 04546412
|
||||||
Node: 04446629
|
Node: 04446773
|
||||||
Node: 04346988
|
Node: 04347132
|
||||||
Node: 04247191
|
Node: 04247335
|
||||||
Node: 04147352
|
Node: 04147496
|
||||||
Node: 0447599
|
Node: 0447743
|
||||||
Node: 03251700
|
Node: 03251844
|
||||||
Node: 03152103
|
Node: 03152247
|
||||||
Node: 0352300
|
Node: 0352444
|
||||||
Node: 02355600
|
Node: 02355744
|
||||||
Node: 02255834
|
Node: 02255978
|
||||||
Node: 02156114
|
Node: 02156258
|
||||||
Node: 0256319
|
Node: 0256463
|
||||||
Node: 0160397
|
Node: 0160541
|
||||||
Node: Notes60498
|
Node: Notes60642
|
||||||
Node: Comparison with Org Agenda searches60660
|
Node: Comparison with Org Agenda searches60804
|
||||||
Node: org-sidebar61549
|
Node: org-sidebar61693
|
||||||
Node: License61828
|
Node: License61972
|
||||||
|
|
||||||
End Tag Table
|
End Tag Table
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue