Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2762 (2024-04-09 19:25:56)

feat!!: Disallow mixing delimiter types in Boolean queries
# Description

Disallow the mixing of `(...)` and `"..."` delimiters in Boolean queries.

This reduces the number of scenarios in which we will accidentally treat a character in the filter as a delimiter.

Since the documentation discouraged use of " as a delimiter, it is unlikely to break any/many real-world searches - but it is a breaking change.

*Note: more improvements are coming so I am not updating the documentation yet.*

### Example change

This mixture used to be allowed:

```text
"not done" AND (is recurring)
```

It now gives the error:

```text
Tasks query: Could not interpret the following instruction as a Boolean combination:
    "not done" AND (is recurring)

The error message is:
    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
```

### Example benefit

For example, it fixes this filter, where previously the closing `"` was wrongly interpreted:

```text
(description includes "hello world") OR (description includes "42")
```

This query used to give:

```text
Could not interpret the following instruction as a Boolean combination:
    (description includes "hello world") OR (description includes "42")

The error message is:
    malformed boolean query -- Unexpected character: " (check the documentation for guidelines)

The instruction was converted to the following simplified line:
    (f1") OR (f2")

Where the sub-expressions in the simplified line are:
    'f1': 'description includes "hello world'
    'f2': 'description includes "42'

For help, see:
    https://publish.obsidian.md/tasks/Queries/Combining+Filters
```

It now works, giving this explanation:

```text
  (description includes "hello world") OR (description includes "42") =>
    OR (At least one of):
      description includes "hello world"
      description includes "42"
```

## Motivation and Context

Reduce the number of pitfalls in Boolean queries.

## How has this been tested?

- Automated tests
- Exploratory testing

## Types of changes

Changes visible to users:

- [x] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
[Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
    - *Will be done later*
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Query/Filter/BooleanDelimiters.ts b/src/Query/Filter/BooleanDelimiters.ts
index 0157ee53..3b167eeb 100644
--- a/src/Query/Filter/BooleanDelimiters.ts
+++ b/src/Query/Filter/BooleanDelimiters.ts
@@ -5,7 +5,9 @@ export function anyOfTheseChars(allowedChars: string): string {
 /**
  * A class to try to identify the type of delimiter used between Boolean operators.
  *
- * Currently under development
+ * Note that this only checks the first and last non-operator characters on the line,
+ * so where there is more than one binary operator, it is still possible for the user
+ * to mix delimiters, and for the error to not be detected until later in the parsing process.
  */
 export class BooleanDelimiters {
     public readonly openFilterChars;
@@ -28,4 +30,31 @@ export class BooleanDelimiters {
     public static allSupportedDelimiters(): BooleanDelimiters {
         return new BooleanDelimiters('("', ')"', '()"');
     }
+
+    public static fromInstructionLine(instruction: string) {
+        const trimmedInstruction = instruction.trim();
+
+        // We use a set of capitals and spaces as a short-cut to match AND, OR, NOT, AND NOT etc.
+        // The only valid initial operator is NOT, so this may be worth tightening up, if
+        // further tests show that it would be worthwhile.
+        const findAnyInitialUnaryOperator = /^[A-Z ]*\s*(.*)/;
+        const matches = findAnyInitialUnaryOperator.exec(trimmedInstruction);
+        if (matches) {
+            const instructionWithoutAnyLeadingOperators = matches[1];
+            const firstChar = instructionWithoutAnyLeadingOperators[0];
+            const lastChar = instructionWithoutAnyLeadingOperators.slice(-1);
+
+            if (firstChar === '(' && lastChar === ')') {
+                return new BooleanDelimiters('(', ')', '()');
+            }
+
+            if (firstChar === '"' && lastChar === '"') {
+                return new BooleanDelimiters('"', '"', '"');
+            }
+        }
+
+        throw new Error(
+            'All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.',
+        );
+    }
 }
diff --git a/src/Query/Filter/BooleanField.ts b/src/Query/Filter/BooleanField.ts
index e6b45ae9..eca72cbb 100644
--- a/src/Query/Filter/BooleanField.ts
+++ b/src/Query/Filter/BooleanField.ts
@@ -75,7 +75,15 @@ export class BooleanField extends Field {
             return FilterOrErrorMessage.fromError(line, 'empty line');
         }
 
-        const parseResult = BooleanPreprocessor.preprocessExpression(line);
+        let delimiters;
+        try {
+            delimiters = BooleanDelimiters.fromInstructionLine(line);
+        } catch (error) {
+            const message = error instanceof Error ? error.message : 'unknown error type';
+            return FilterOrErrorMessage.fromError(line, this.helpMessageFromSimpleError(line, message));
+        }
+
+        const parseResult = BooleanPreprocessor.preprocessExpression(line, delimiters);
         const simplifiedLine = parseResult.simplifiedLine;
         const filters = parseResult.filters;
         try {
diff --git a/src/Query/Filter/BooleanPreprocessor.ts b/src/Query/Filter/BooleanPreprocessor.ts
index c5c13543..dea7fd03 100644
--- a/src/Query/Filter/BooleanPreprocessor.ts
+++ b/src/Query/Filter/BooleanPreprocessor.ts
@@ -6,14 +6,12 @@ export type BooleanPreprocessorResult = {
 };
 
 export class BooleanPreprocessor {
-    public static preprocessExpression(line: string): BooleanPreprocessorResult {
-        const parts = BooleanPreprocessor.splitLine(line);
-        return BooleanPreprocessor.getFiltersAndSimplifiedLine(parts);
+    public static preprocessExpression(line: string, delimiters: BooleanDelimiters): BooleanPreprocessorResult {
+        const parts = BooleanPreprocessor.splitLine(line, delimiters);
+        return BooleanPreprocessor.getFiltersAndSimplifiedLine(parts, delimiters);
     }
 
-    public static splitLine(line: string) {
-        const delimiters = BooleanDelimiters.allSupportedDelimiters();
-
+    public static splitLine(line: string, delimiters: BooleanDelimiters) {
         // Here, we split the input line in to separate operators-plus-adjacent-delimiters
         // and the remaining filter text.
 
@@ -69,9 +67,7 @@ export class BooleanPreprocessor {
             .filter((substring) => substring !== '');
     }
 
-    private static getFiltersAndSimplifiedLine(parts: string[]) {
-        const delimiters = BooleanDelimiters.allSupportedDelimiters();
-
+    private static getFiltersAndSimplifiedLine(parts: string[], delimiters: BooleanDelimiters) {
         // Holds the reconstructed expression with placeholders
         let simplifiedLine = '';
         let currentIndex = 1; // Placeholder index starts at 1


Test patch:
diff --git a/tests/Query/Filter/BooleanDelimiters.test.ts b/tests/Query/Filter/BooleanDelimiters.test.ts
index 4caccd2b..cbcf99f3 100644
--- a/tests/Query/Filter/BooleanDelimiters.test.ts
+++ b/tests/Query/Filter/BooleanDelimiters.test.ts
@@ -1,5 +1,31 @@
 import { BooleanDelimiters } from '../../../src/Query/Filter/BooleanDelimiters';
 
+function shouldDelimitWithParentheses(line: string) {
+    const delimiters = BooleanDelimiters.fromInstructionLine(line);
+
+    expect(delimiters.openFilterChars).toEqual('(');
+    expect(delimiters.closeFilterChars).toEqual(')');
+    expect(delimiters.openAndCloseFilterChars).toEqual('()');
+}
+
+function shouldDelimitWithDoubleQuotes(line: string) {
+    const delimiters = BooleanDelimiters.fromInstructionLine(line);
+
+    expect(delimiters.openFilterChars).toEqual('"');
+    expect(delimiters.closeFilterChars).toEqual('"');
+    expect(delimiters.openAndCloseFilterChars).toEqual('"');
+}
+
+function shouldThrow(line: string) {
+    const t = () => {
+        BooleanDelimiters.fromInstructionLine(line);
+    };
+    expect(t).toThrow(Error);
+    expect(t).toThrowError(
+        'All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.',
+    );
+}
+
 describe('BooleanDelimiters', () => {
     it('construction - all delimiters', () => {
         const delimiters = BooleanDelimiters.allSupportedDelimiters();
@@ -12,4 +38,51 @@ describe('BooleanDelimiters', () => {
 
         expect(delimiters.openAndCloseFilterChars).toEqual('()"');
     });
+
+    describe('construct from line with binary operators', () => {
+        it('should recognise () delimiters', () => {
+            shouldDelimitWithParentheses('(not done) OR (done)');
+        });
+
+        it('does not recognise inconsistent delimiters in middle of line', () => {
+            shouldDelimitWithParentheses('(not done) OR "done" OR (done)');
+        });
+
+        it('should recognise "" delimiters', () => {
+            shouldDelimitWithDoubleQuotes('"not done" OR "done"');
+        });
+
+        it('should reject line with mixed delimiters', () => {
+            shouldThrow('(not done) OR "done"');
+        });
+
+        it('should reject line with unknown delimiters', () => {
+            shouldThrow('{not done} OR {done}');
+        });
+    });
+
+    describe('construct from line starting with NOT', () => {
+        it('should recognise () delimiters', () => {
+            shouldDelimitWithParentheses('NOT (not done)');
+        });
+
+        it('should recognise "" delimiters', () => {
+            shouldDelimitWithDoubleQuotes('NOT "not done"');
+        });
+
+        it('should reject line with mixed delimiters', () => {
+            shouldThrow('NOT (not done"');
+        });
+
+        it('should reject line with unknown delimiters', () => {
+            shouldThrow('NOT {not done}');
+        });
+    });
+
+    describe('error cases', () => {
+        it('should throw if line is too short', () => {
+            shouldThrow('');
+            shouldThrow('x');
+        });
+    });
 });
diff --git a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
index 598035fa..61890bbe 100644
--- a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
+++ b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
@@ -100,11 +100,11 @@ Input:
 '"not done" AND (is recurring)'
 =>
 Result:
-  "not done" AND (is recurring) =>
-    AND (All of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    "not done" AND (is recurring)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -113,11 +113,11 @@ Input:
 '"not done" OR (is recurring)'
 =>
 Result:
-  "not done" OR (is recurring) =>
-    OR (At least one of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    "not done" OR (is recurring)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -126,11 +126,11 @@ Input:
 '"not done" XOR (is recurring)'
 =>
 Result:
-  "not done" XOR (is recurring) =>
-    XOR (Exactly one of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    "not done" XOR (is recurring)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -139,12 +139,11 @@ Input:
 '"not done" AND NOT (is recurring)'
 =>
 Result:
-  "not done" AND NOT (is recurring) =>
-    AND (All of):
-      not done
-      NOT:
-        is recurring
+Could not interpret the following instruction as a Boolean combination:
+    "not done" AND NOT (is recurring)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -153,12 +152,11 @@ Input:
 '"not done" OR NOT (is recurring)'
 =>
 Result:
-  "not done" OR NOT (is recurring) =>
-    OR (At least one of):
-      not done
-      NOT:
-        is recurring
+Could not interpret the following instruction as a Boolean combination:
+    "not done" OR NOT (is recurring)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -168,11 +166,11 @@ Input:
 '(not done) AND "is recurring"'
 =>
 Result:
-  (not done) AND "is recurring" =>
-    AND (All of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    (not done) AND "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -181,11 +179,11 @@ Input:
 '((not done)) AND "is recurring"'
 =>
 Result:
-  ((not done)) AND "is recurring" =>
-    AND (All of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    ((not done)) AND "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -194,11 +192,11 @@ Input:
 '(not done) OR "is recurring"'
 =>
 Result:
-  (not done) OR "is recurring" =>
-    OR (At least one of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    (not done) OR "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -207,11 +205,11 @@ Input:
 '(not done) XOR "is recurring"'
 =>
 Result:
-  (not done) XOR "is recurring" =>
-    XOR (Exactly one of):
-      not done
-      is recurring
+Could not interpret the following instruction as a Boolean combination:
+    (not done) XOR "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -220,12 +218,11 @@ Input:
 '(not done) AND NOT "is recurring"'
 =>
 Result:
-  (not done) AND NOT "is recurring" =>
-    AND (All of):
-      not done
-      NOT:
-        is recurring
+Could not interpret the following instruction as a Boolean combination:
+    (not done) AND NOT "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -234,12 +231,11 @@ Input:
 '(not done) OR NOT "is recurring"'
 =>
 Result:
-  (not done) OR NOT "is recurring" =>
-    OR (At least one of):
-      not done
-      NOT:
-        is recurring
+Could not interpret the following instruction as a Boolean combination:
+    (not done) OR NOT "is recurring"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -249,11 +245,11 @@ Input:
 '"HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL)'
 =>
 Result:
-  "HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL) =>
-    OR (At least one of):
-      HAS DUE DATE
-      DESCRIPTION INCLUDES SPECIAL
+Could not interpret the following instruction as a Boolean combination:
+    "HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -275,11 +271,11 @@ Input:
 '"has due date" OR (description includes special)'
 =>
 Result:
-  "has due date" OR (description includes special) =>
-    OR (At least one of):
-      has due date
-      description includes special
+Could not interpret the following instruction as a Boolean combination:
+    "has due date" OR (description includes special)
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -585,11 +581,11 @@ Input:
 '(HAS START DATE) AND "DESCRIPTION INCLUDES SOME"'
 =>
 Result:
-  (HAS START DATE) AND "DESCRIPTION INCLUDES SOME" =>
-    AND (All of):
-      HAS START DATE
-      DESCRIPTION INCLUDES SOME
+Could not interpret the following instruction as a Boolean combination:
+    (HAS START DATE) AND "DESCRIPTION INCLUDES SOME"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
@@ -887,21 +883,10 @@ Input:
 '(description includes "hello world") OR (description includes "42")'
 =>
 Result:
-Could not interpret the following instruction as a Boolean combination:
-    (description includes "hello world") OR (description includes "42")
-
-The error message is:
-    malformed boolean query -- Unexpected character: " (check the documentation for guidelines)
-
-The instruction was converted to the following simplified line:
-    (f1") OR (f2")
-
-Where the sub-expressions in the simplified line are:
-    'f1': 'description includes "hello world'
-    'f2': 'description includes "42'
-
-For help, see:
-    https://publish.obsidian.md/tasks/Queries/Combining+Filters
+  (description includes "hello world") OR (description includes "42") =>
+    OR (At least one of):
+      description includes "hello world"
+      description includes "42"
 
 
 --------------------------------------------------------
@@ -1760,11 +1745,11 @@ Input:
 '(has start date) AND "description includes some"'
 =>
 Result:
-  (has start date) AND "description includes some" =>
-    AND (All of):
-      has start date
-      description includes some
+Could not interpret the following instruction as a Boolean combination:
+    (has start date) AND "description includes some"
 
+The error message is:
+    All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.
 
 --------------------------------------------------------
 
diff --git a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt
index 0590ef55..528165f0 100644
--- a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt
+++ b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt
@@ -112,13 +112,9 @@ Input:
 '"not done" AND (is recurring)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" AND (f2)",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -127,13 +123,9 @@ Input:
 '"not done" OR (is recurring)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" OR (f2)",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -142,13 +134,9 @@ Input:
 '"not done" XOR (is recurring)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" XOR (f2)",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -157,13 +145,9 @@ Input:
 '"not done" AND NOT (is recurring)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" AND NOT (f2)",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -172,13 +156,9 @@ Input:
 '"not done" OR NOT (is recurring)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" OR NOT (f2)",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -188,13 +168,9 @@ Input:
 '(not done) AND "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) AND \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -203,13 +179,9 @@ Input:
 '((not done)) AND "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "((f1)) AND \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -218,13 +190,9 @@ Input:
 '(not done) OR "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) OR \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -233,13 +201,9 @@ Input:
 '(not done) XOR "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) XOR \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -248,13 +212,9 @@ Input:
 '(not done) AND NOT "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) AND NOT \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -263,13 +223,9 @@ Input:
 '(not done) OR NOT "is recurring"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) OR NOT \"f2\"",
-    "filters": {
-        "f1": "not done",
-        "f2": "is recurring"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -279,13 +235,9 @@ Input:
 '"HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" OR (f2)",
-    "filters": {
-        "f1": "HAS DUE DATE",
-        "f2": "DESCRIPTION INCLUDES SPECIAL"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -309,13 +261,9 @@ Input:
 '"has due date" OR (description includes special)'
 =>
 Result:
-{
-    "simplifiedLine": "\"f1\" OR (f2)",
-    "filters": {
-        "f1": "has due date",
-        "f2": "description includes special"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -612,13 +560,9 @@ Input:
 '(HAS START DATE) AND "DESCRIPTION INCLUDES SOME"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) AND \"f2\"",
-    "filters": {
-        "f1": "HAS START DATE",
-        "f2": "DESCRIPTION INCLUDES SOME"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -900,10 +844,10 @@ Input:
 =>
 Result:
 {
-    "simplifiedLine": "(f1\") OR (f2\")",
+    "simplifiedLine": "(f1) OR (f2)",
     "filters": {
-        "f1": "description includes \"hello world",
-        "f2": "description includes \"42"
+        "f1": "description includes \"hello world\"",
+        "f2": "description includes \"42\""
     }
 }
 
@@ -1757,13 +1701,9 @@ Input:
 '(has start date) AND "description includes some"'
 =>
 Result:
-{
-    "simplifiedLine": "(f1) AND \"f2\"",
-    "filters": {
-        "f1": "has start date",
-        "f2": "description includes some"
-    }
-}
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
diff --git a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_split_line.approved.txt b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_split_line.approved.txt
index c18b5e9d..5ff9ada3 100644
--- a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_split_line.approved.txt
+++ b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_split_line.approved.txt
@@ -117,14 +117,9 @@ Input:
 '"not done" AND (is recurring)'
 =>
 Result:
-[
-    "\"",
-    "not done",
-    "\" ",
-    "AND (",
-    "is recurring",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -133,14 +128,9 @@ Input:
 '"not done" OR (is recurring)'
 =>
 Result:
-[
-    "\"",
-    "not done",
-    "\" ",
-    "OR (",
-    "is recurring",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -149,14 +139,9 @@ Input:
 '"not done" XOR (is recurring)'
 =>
 Result:
-[
-    "\"",
-    "not done",
-    "\" ",
-    "XOR (",
-    "is recurring",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -165,16 +150,9 @@ Input:
 '"not done" AND NOT (is recurring)'
 =>
 Result:
-[
-    "\"",
-    "not done",
-    "\" ",
-    "AND",
-    " ",
-    "NOT (",
-    "is recurring",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -183,16 +161,9 @@ Input:
 '"not done" OR NOT (is recurring)'
 =>
 Result:
-[
-    "\"",
-    "not done",
-    "\" ",
-    "OR",
-    " ",
-    "NOT (",
-    "is recurring",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -202,14 +173,9 @@ Input:
 '(not done) AND "is recurring"'
 =>
 Result:
-[
-    "(",
-    "not done",
-    ") AND",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -218,15 +184,9 @@ Input:
 '((not done)) AND "is recurring"'
 =>
 Result:
-[
-    "((",
-    "not done",
-    ")",
-    ") AND",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -235,14 +195,9 @@ Input:
 '(not done) OR "is recurring"'
 =>
 Result:
-[
-    "(",
-    "not done",
-    ") OR",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -251,14 +206,9 @@ Input:
 '(not done) XOR "is recurring"'
 =>
 Result:
-[
-    "(",
-    "not done",
-    ") XOR",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -267,16 +217,9 @@ Input:
 '(not done) AND NOT "is recurring"'
 =>
 Result:
-[
-    "(",
-    "not done",
-    ") AND",
-    " ",
-    "NOT",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -285,16 +228,9 @@ Input:
 '(not done) OR NOT "is recurring"'
 =>
 Result:
-[
-    "(",
-    "not done",
-    ") OR",
-    " ",
-    "NOT",
-    " \"",
-    "is recurring",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -304,14 +240,9 @@ Input:
 '"HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL)'
 =>
 Result:
-[
-    "\"",
-    "HAS DUE DATE",
-    "\" ",
-    "OR (",
-    "DESCRIPTION INCLUDES SPECIAL",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -337,14 +268,9 @@ Input:
 '"has due date" OR (description includes special)'
 =>
 Result:
-[
-    "\"",
-    "has due date",
-    "\" ",
-    "OR (",
-    "description includes special",
-    ")"
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -739,14 +665,9 @@ Input:
 '(HAS START DATE) AND "DESCRIPTION INCLUDES SOME"'
 =>
 Result:
-[
-    "(",
-    "HAS START DATE",
-    ") AND",
-    " \"",
-    "DESCRIPTION INCLUDES SOME",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
@@ -1063,11 +984,10 @@ Input:
 Result:
 [
     "(",
-    "description includes \"hello world",
-    "\"",
+    "description includes \"hello world\"",
     ") OR (",
-    "description includes \"42",
-    "\")"
+    "description includes \"42\"",
+    ")"
 ]
 
 --------------------------------------------------------
@@ -2019,14 +1939,9 @@ Input:
 '(has start date) AND "description includes some"'
 =>
 Result:
-[
-    "(",
-    "has start date",
-    ") AND",
-    " \"",
-    "description includes some",
-    "\""
-]
+Error: Parsing expression.
+The error message was:
+    "Error: All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported."
 
 --------------------------------------------------------
 
diff --git a/tests/Query/Filter/BooleanField.test.ts b/tests/Query/Filter/BooleanField.test.ts
index 8fe47f60..cca346a0 100644
--- a/tests/Query/Filter/BooleanField.test.ts
+++ b/tests/Query/Filter/BooleanField.test.ts
@@ -161,23 +161,11 @@ describe('boolean query - filter', () => {
             it('should allow ( and ) as delimiters around 2 filters - with " inside', () => {
                 // This searches for words surrounded by double-quotes:
                 const filter = createValidFilter('(description includes "hello world") OR (description includes "42")');
-                // TODO Fix this:
                 expect(explanationOrError(filter)).toMatchInlineSnapshot(`
-                    "Could not interpret the following instruction as a Boolean combination:
-                        (description includes "hello world") OR (description includes "42")
-
-                    The error message is:
-                        malformed boolean query -- Unexpected character: " (check the documentation for guidelines)
-
-                    The instruction was converted to the following simplified line:
-                        (f1") OR (f2")
-
-                    Where the sub-expressions in the simplified line are:
-                        'f1': 'description includes "hello world'
-                        'f2': 'description includes "42'
-
-                    For help, see:
-                        https://publish.obsidian.md/tasks/Queries/Combining+Filters
+                    "(description includes "hello world") OR (description includes "42") =>
+                      OR (At least one of):
+                        description includes "hello world"
+                        description includes "42"
                     "
                 `);
             });
@@ -206,6 +194,13 @@ describe('boolean query - filter', () => {
                             `);
             });
         });
+
+        describe('Mixed delimiters', () => {
+            it('should not allow a mixture of () and "" delimiters any more - breaking change in 7.0.0', () => {
+                const filter = new BooleanField().createFilterOrErrorMessage('(not done) AND "is recurring"');
+                expect(filter.error).toBeDefined();
+            });
+        });
     });
 
     describe('error cases - to show error messages', () => {
diff --git a/tests/Query/Filter/BooleanFieldVerify.ts b/tests/Query/Filter/BooleanFieldVerify.ts
index 1fb3d91b..42969a47 100644
--- a/tests/Query/Filter/BooleanFieldVerify.ts
+++ b/tests/Query/Filter/BooleanFieldVerify.ts
@@ -1,6 +1,7 @@
 import { verifyAll } from 'approvals/lib/Providers/Jest/JestApprovals';
 import { errorMessageForException } from '../../../src/lib/ExceptionTools';
 import { BooleanField } from '../../../src/Query/Filter/BooleanField';
+import { BooleanDelimiters } from '../../../src/Query/Filter/BooleanDelimiters';
 
 export const inputs = `
 (not done) AND (is recurring)
@@ -221,7 +222,7 @@ NOT(path includes b)
 OR (description includes d1)
 `;
 
-export function verifyBooleanExpressionPreprocessing(fn: (text: string) => any) {
+export function verifyBooleanExpressionPreprocessing(fn: (text: string, delimiters: BooleanDelimiters) => any) {
     verifyAll('Results of preprocessing boolean expressions', inputs.split('\n'), (input) => {
         if (input.trim() === '') {
             return '';
@@ -229,7 +230,8 @@ export function verifyBooleanExpressionPreprocessing(fn: (text: string) => any)
 
         let result: string = '';
         try {
-            result = JSON.stringify(fn(input), null, 4);
+            const delimiters = BooleanDelimiters.fromInstructionLine(input);
+            result = JSON.stringify(fn(input, delimiters), null, 4);
         } catch (e) {
             result = errorMessageForException('Parsing expression', e);
         }
diff --git a/tests/Query/Filter/BooleanPreprocessor.test.ts b/tests/Query/Filter/BooleanPreprocessor.test.ts
index ec538028..10f003ae 100644
--- a/tests/Query/Filter/BooleanPreprocessor.test.ts
+++ b/tests/Query/Filter/BooleanPreprocessor.test.ts
@@ -1,7 +1,8 @@
 import { BooleanPreprocessor } from '../../../src/Query/Filter/BooleanPreprocessor';
+import { BooleanDelimiters } from '../../../src/Query/Filter/BooleanDelimiters';
 
 function preprocess(line: string) {
-    return BooleanPreprocessor.preprocessExpression(line);
+    return BooleanPreprocessor.preprocessExpression(line, BooleanDelimiters.allSupportedDelimiters());
 }
 
 describe('BooleanPreprocessor', () => {
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index 75226e16..6250a561 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -998,7 +998,7 @@ describe('Query', () => {
             [
                 'simple OR',
                 {
-                    filters: ['"has due date" OR (description includes special)'],
+                    filters: ['(has due date) OR (description includes special)'],
                     tasks: [
                         '- [ ] task 1',
                         '- [ ] task 2 🛫 2022-04-20 ⏳ 2022-04-20 📅 2022-04-20',
@@ -1015,7 +1015,7 @@ describe('Query', () => {
             [
                 'simple AND',
                 {
-                    filters: ['(has start date) AND "description includes some"'],
+                    filters: ['(has start date) AND (description includes some)'],
                     tasks: [
                         '- [ ] task 1',
                         '- [ ] some task 2 🛫 2022-04-20 ⏳ 2022-04-20 📅 2022-04-20',