Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All: Filter "notebook" can now be negated #4651

Merged
merged 2 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ You can also use search filters to further restrict the search.
|**any:**|Return notes that satisfy any/all of the required conditions. `any:0` is the default, which means all conditions must be satisfied.|`any:1 cat dog` will return notes that have the word `cat` or `dog`.<br>`any:0 cat dog` will return notes with both the words `cat` and `dog`. |
| **title:** <br> **body:**|Restrict your search to just the title or the body field.|`title:"hello world"` searches for notes whose title contains `hello` and `world`.<br>`title:hello -body:world` searches for notes whose title contains `hello` and body does not contain `world`.
| **tag:** |Restrict the search to the notes with the specified tags.|`tag:office` searches for all notes having tag office.<br>`tag:office tag:important` searches for all notes having both office and important tags.<br>`tag:office -tag:spam` searches for notes having tag `office` which do not have tag `spam`.<br>`any:1 tag:office tag:spam` searches for notes having tag `office` or tag `spam`.<br>`tag:be*ful` does a search with wildcards.<br>`tag:*` returns all notes with tags.<br>`-tag:*` returns all notes without tags.|
| **notebook:** | Restrict the search to the specified notebook(s). It cannot be negated. |`notebook:books` limits the search scope within `books` and all its subnotebooks.<br>`notebook:wheel*time` does a wildcard search.|
| **notebook:** | Restrict the search to the specified notebook(s). |`notebook:books` limits the search scope within `books` and all its subnotebooks.<br>`notebook:wheel*time` does a wildcard search.|
| **created:** <br> **updated:** | Searches for notes created/updated on dates specified using YYYYMMDD format. You can also search relative to the current day, week, month, or year. | `created:20201218` will return notes created on or after December 18, 2020.<br>`-updated:20201218` will return notes updated before December 18, 2020.<br>`created:20200118 -created:20201215` will return notes created between January 18, 2020, and before December 15, 2020.<br>`created:202001 -created:202003` will return notes created on or after January and before March 2020.<br>`updated:1997 -updated:2020` will return all notes updated between the years 1997 and 2019.<br>`created:day-2` searches for all notes created in the past two days.<br>`updated:year-0` searches all notes updated in the current year.
| **type:** |Restrict the search to either notes or todos. | `type:note` to return all notes<br>`type:todo` to return all todos |
| **iscompleted:** | Restrict the search to either completed or uncompleted todos. | `iscompleted:1` to return all completed todos<br>`iscompleted:0` to return all uncompleted todos|
Expand Down
2 changes: 0 additions & 2 deletions packages/app-cli/tests/filterParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ describe('filterParser should be correct filter for keyword', () => {
searchString = 'iscompleted:blah';
expect(() => filterParser(searchString)).toThrow(new Error('The value of filter "iscompleted" must be "1" or "0"'));

searchString = '-notebook:n1';
expect(() => filterParser(searchString)).toThrow(new Error('notebook can\'t be negated'));

searchString = '-iscompleted:1';
expect(() => filterParser(searchString)).toThrow(new Error('iscompleted can\'t be negated'));
Expand Down
48 changes: 48 additions & 0 deletions packages/app-cli/tests/services_SearchFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -742,4 +742,52 @@ describe('services_SearchFilter', function() {

}));

it('should support negating notebooks', (async () => {

const folder1 = await Folder.save({ title: 'folder1' });
let n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: folder1.id });
let n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: folder1.id });


const folder2 = await Folder.save({ title: 'folder2' });
let n3 = await Note.save({ title: 'task3', body: 'baz', parent_id: folder2.id });
let n4 = await Note.save({ title: 'task4', body: 'blah', parent_id: folder2.id });


await engine.syncTables();

let rows = await engine.search('-notebook:folder1');
expect(rows.length).toBe(2);
expect(ids(rows)).toContain(n3.id);
expect(ids(rows)).toContain(n4.id);


rows = await engine.search('-notebook:folder2');
expect(rows.length).toBe(2);
expect(ids(rows)).toContain(n1.id);
expect(ids(rows)).toContain(n2.id);

}));

it('should support both inclusion and exclusion of notebooks together', (async () => {

const parentFolder = await Folder.save({ title: 'parent' });
let n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: parentFolder.id });
let n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: parentFolder.id });


const subFolder = await Folder.save({ title: 'child', parent_id: parentFolder.id });
let n3 = await Note.save({ title: 'task3', body: 'baz', parent_id: subFolder.id });
let n4 = await Note.save({ title: 'task4', body: 'blah', parent_id: subFolder.id });


await engine.syncTables();

let rows = await engine.search('notebook:parent -notebook:child');
expect(rows.length).toBe(2);
expect(ids(rows)).toContain(n1.id);
expect(ids(rows)).toContain(n2.id);

}));

});
2 changes: 1 addition & 1 deletion packages/lib/services/searchengine/filterParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const parseQuery = (query: string): Term[] => {
}

// validation
let incorrect = result.filter(term => term.name === 'type' || term.name === 'iscompleted' || term.name === 'notebook')
let incorrect = result.filter(term => term.name === 'type' || term.name === 'iscompleted')
.find(x => x.negated);
if (incorrect) throw new Error(`${incorrect.name} can't be negated`);

Expand Down
30 changes: 22 additions & 8 deletions packages/lib/services/searchengine/queryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@ enum Requirement {
INCLUSION = 'INCLUSION',
}

const notebookFilter = (terms: Term[], conditions: string[], params: string[], withs: string[]) => {
const notebooks = terms.filter(x => x.name === 'notebook' && !x.negated).map(x => x.value);
const _notebookFilter = (notebooks: string[], requirement: Requirement, conditions: string[], params: string[], withs: string[]) => {
if (notebooks.length === 0) return;

const likes = [];
for (let i = 0; i < notebooks.length; i++) {
likes.push('folders.title LIKE ?');
}

const relevantFolders = likes.join(' OR ');

const viewName = requirement === Requirement.EXCLUSION ? 'notebooks_not_in_scope' : 'notebooks_in_scope';
const withInNotebook = `
notebooks_in_scope(id)
${viewName}(id)
AS (
SELECT folders.id
FROM folders
Expand All @@ -45,22 +46,35 @@ const notebookFilter = (terms: Term[], conditions: string[], params: string[], w
UNION ALL
SELECT folders.id
FROM folders
JOIN notebooks_in_scope
ON folders.parent_id=notebooks_in_scope.id
JOIN ${viewName}
ON folders.parent_id=${viewName}.id
)`;

const where = `
AND ROWID IN (
AND ROWID ${requirement === Requirement.EXCLUSION ? 'NOT' : ''} IN (
SELECT notes_normalized.ROWID
FROM notebooks_in_scope
FROM ${viewName}
JOIN notes_normalized
ON notebooks_in_scope.id=notes_normalized.parent_id
ON ${viewName}.id=notes_normalized.parent_id
)`;


withs.push(withInNotebook);
params.push(...notebooks);
conditions.push(where);

};

const notebookFilter = (terms: Term[], conditions: string[], params: string[], withs: string[]) => {
const notebooksToInclude = terms.filter(x => x.name === 'notebook' && !x.negated).map(x => x.value);
_notebookFilter(notebooksToInclude, Requirement.INCLUSION, conditions, params, withs);

const notebooksToExclude = terms.filter(x => x.name === 'notebook' && x.negated).map(x => x.value);
_notebookFilter(notebooksToExclude, Requirement.EXCLUSION, conditions, params, withs);
};



const getOperator = (requirement: Requirement, relation: Relation): Operation => {
if (relation === 'AND' && requirement === 'INCLUSION') { return Operation.INTERSECT; } else { return Operation.UNION; }
};
Expand Down