diff --git a/db/demo.zip b/db/demo.zip index b811a56be..0449e6a5e 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/spec/search/lexer.spec.js b/spec/search/lexer.spec.js index d19d26994..854e785eb 100644 --- a/spec/search/lexer.spec.js +++ b/spec/search/lexer.spec.js @@ -87,14 +87,16 @@ describe("Lexer expression", () => { .toEqual(["#label", "*=*", "text"]); }); - it("simple label operator with in quotes and without", () => { + it("simple label operator with in quotes", () => { expect(lex("#label*=*'text'").expressionTokens) .toEqual([ {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, {token: "text", inQuotes: true, startIndex: 10, endIndex: 13} ]); + }); + it("simple label operator with param without quotes", () => { expect(lex("#label*=*text").expressionTokens) .toEqual([ {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, @@ -103,6 +105,16 @@ describe("Lexer expression", () => { ]); }); + it("simple label operator with empty string param", () => { + expect(lex("#label = ''").expressionTokens) + .toEqual([ + {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, + {token: "=", inQuotes: false, startIndex: 7, endIndex: 7}, + // weird case for empty strings which ends up with endIndex < startIndex :-( + {token: "", inQuotes: true, startIndex: 10, endIndex: 9} + ]); + }); + it("note. prefix also separates fulltext from expression", () => { expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) .toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index 556986446..132db286e 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -19,6 +19,13 @@ function tokens(toks, cur = 0) { }); } +function assertIsArchived(exp) { + expect(exp.constructor.name).toEqual("PropertyComparisonExp"); + expect(exp.propertyName).toEqual("isArchived"); + expect(exp.operator).toEqual("="); + expect(exp.comparedValue).toEqual("false"); +} + describe("Parser", () => { it("fulltext parser without content", () => { const rootExp = parse({ @@ -29,8 +36,9 @@ describe("Parser", () => { expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); - expect(rootExp.subExpressions[1].constructor.name).toEqual("NoteCacheFlatTextExp"); - expect(rootExp.subExpressions[1].tokens).toEqual(["hello", "hi"]); + expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); + expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp"); + expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]); }); it("fulltext parser with content", () => { @@ -40,9 +48,12 @@ describe("Parser", () => { searchContext: new SearchContext({includeNoteContent: true}) }); - expect(rootExp.constructor.name).toEqual("OrExp"); + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); - const subs = rootExp.subExpressions; + expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); + + const subs = rootExp.subExpressions[1].subExpressions; expect(subs[0].constructor.name).toEqual("NoteCacheFlatTextExp"); expect(subs[0].tokens).toEqual(["hello", "hi"]); @@ -61,10 +72,12 @@ describe("Parser", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.attributeType).toEqual("label"); - expect(rootExp.attributeName).toEqual("mylabel"); - expect(rootExp.comparator).toBeTruthy(); + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); + expect(rootExp.subExpressions[1].attributeType).toEqual("label"); + expect(rootExp.subExpressions[1].attributeName).toEqual("mylabel"); + expect(rootExp.subExpressions[1].comparator).toBeTruthy(); }); it("simple attribute negation", () => { @@ -74,10 +87,12 @@ describe("Parser", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("NotExp"); - expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpression.attributeType).toEqual("label"); - expect(rootExp.subExpression.attributeName).toEqual("mylabel"); + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); + expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("label"); + expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("mylabel"); rootExp = parse({ fulltextTokens: [], @@ -85,10 +100,12 @@ describe("Parser", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("NotExp"); - expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpression.attributeType).toEqual("relation"); - expect(rootExp.subExpression.attributeName).toEqual("myrelation"); + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); + expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("relation"); + expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("myrelation"); }); it("simple label AND", () => { @@ -99,7 +116,10 @@ describe("Parser", () => { }); expect(rootExp.constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions; + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); + const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); @@ -116,7 +136,10 @@ describe("Parser", () => { }); expect(rootExp.constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions; + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); + const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); @@ -132,8 +155,11 @@ describe("Parser", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions; + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); + const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); @@ -155,8 +181,9 @@ describe("Parser", () => { expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); expect(firstSub.propertyName).toEqual('isArchived'); - expect(secondSub.constructor.name).toEqual("NoteCacheFlatTextExp"); - expect(secondSub.tokens).toEqual(["hello"]); + expect(secondSub.constructor.name).toEqual("OrExp"); + expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp"); + expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]); expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); expect(thirdSub.attributeName).toEqual("mylabel"); @@ -169,8 +196,11 @@ describe("Parser", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions; + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); + const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); @@ -232,10 +262,12 @@ describe("Invalid expressions", () => { searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.attributeType).toEqual("label"); - expect(rootExp.attributeName).toEqual("first"); - expect(rootExp.comparator).toBeTruthy(); + expect(rootExp.constructor.name).toEqual("AndExp"); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); + expect(rootExp.subExpressions[1].attributeType).toEqual("label"); + expect(rootExp.subExpressions[1].attributeName).toEqual("first"); + expect(rootExp.subExpressions[1].comparator).toBeTruthy(); }); it("searching by relation without note property", () => { diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js index b95966b8c..2cdc00e57 100644 --- a/spec/search/search.spec.js +++ b/spec/search/search.spec.js @@ -562,8 +562,8 @@ describe("Search", () => { expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria"); expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy"); - searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', searchContext); - expect(searchResults.length).toEqual(0); + searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); + expect(searchResults.length).toEqual(1); searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); expect(searchResults.length).toEqual(4); diff --git a/spec/search/value_extractor.spec.js b/spec/search/value_extractor.spec.js index d82b50ce4..6262ecffb 100644 --- a/spec/search/value_extractor.spec.js +++ b/spec/search/value_extractor.spec.js @@ -1,6 +1,9 @@ const {note} = require('./note_cache_mocking.js'); const ValueExtractor = require('../../src/services/search/value_extractor.js'); const noteCache = require('../../src/services/note_cache/note_cache.js'); +const SearchContext = require("../../src/services/search/search_context.js"); + +const dsc = new SearchContext(); describe("Value extractor", () => { beforeEach(() => { @@ -10,7 +13,7 @@ describe("Value extractor", () => { it("simple title extraction", async () => { const europe = note("Europe").note; - const valueExtractor = new ValueExtractor(["note", "title"]); + const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(europe)).toEqual("Europe"); @@ -21,12 +24,12 @@ describe("Value extractor", () => { .label("Capital", "Vienna") .note; - let valueExtractor = new ValueExtractor(["note", "labels", "capital"]); + let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(austria)).toEqual("Vienna"); - valueExtractor = new ValueExtractor(["#capital"]); + valueExtractor = new ValueExtractor(dsc, ["#capital"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(austria)).toEqual("Vienna"); @@ -38,12 +41,12 @@ describe("Value extractor", () => { .child(note("Austria") .child(vienna)); - let valueExtractor = new ValueExtractor(["note", "children", "children", "title"]); + let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); - valueExtractor = new ValueExtractor(["note", "parents", "parents", "title"]); + valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); @@ -56,12 +59,12 @@ describe("Value extractor", () => { .relation('neighbor', czechRepublic.note) .relation('neighbor', slovakia.note); - let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]); + let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(austria.note)).toEqual("Prague"); - valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]); + valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.extract(austria.note)).toEqual("Prague"); @@ -70,17 +73,17 @@ describe("Value extractor", () => { describe("Invalid value extractor property path", () => { it('each path must start with "note" (or label/relation)', - () => expect(new ValueExtractor(["neighbor"]).validate()).toBeTruthy()); + () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy()); it("extra path element after terminal label", - () => expect(new ValueExtractor(["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); + () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); it("extra path element after terminal title", - () => expect(new ValueExtractor(["note", "title", "isProtected"]).validate()).toBeTruthy()); + () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy()); it("relation name and note property is missing", - () => expect(new ValueExtractor(["note", "relations"]).validate()).toBeTruthy()); + () => expect(new ValueExtractor(dsc, ["note", "relations"]).validate()).toBeTruthy()); it("relation is specified but target note property is not specified", - () => expect(new ValueExtractor(["note", "relations", "myrel"]).validate()).toBeTruthy()); + () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy()); }); diff --git a/src/services/search/services/lex.js b/src/services/search/services/lex.js index bb80876e5..5234900f7 100644 --- a/src/services/search/services/lex.js +++ b/src/services/search/services/lex.js @@ -21,8 +21,8 @@ function lex(str) { } } - function finishWord(endIndex) { - if (currentWord === '') { + function finishWord(endIndex, createAlsoForEmptyWords = false) { + if (currentWord === '' && !createAlsoForEmptyWords) { return; } @@ -71,7 +71,7 @@ function lex(str) { } } else if (quotes === chr) { - finishWord(i - 1); + finishWord(i - 1, true); quotes = false; }