feat(search): tighten the grammar and use Unicode case folding

This commit is contained in:
yyc12345 committed 2026-10-04 16:10:14 +08:00
1 parent 97f79db3f1
commit 78e5faa3c4
7 files changed
+305 -105

No files matched your search

+90 -7
View File
@@ -130,8 +130,9 @@ public class SearchQueryParserTests {
}
[TestMethod]
public void QualifierValueMayContainColons() {
AssertParsesTo(new SearchNode.TagName("a:b"), "tag:a:b");
public void QuotedQualifierValueMayContainColonsAndOperators() {
AssertParsesTo(new SearchNode.TagName("a:b"), "tag:\"a:b\"");
AssertParsesTo(new SearchNode.TagName("AND"), "tag:\"AND\"");
}
[TestMethod]
@@ -148,12 +149,18 @@ public class SearchQueryParserTests {
[TestMethod]
public void IdQualifierParsesNumber() {
AssertParsesTo(new SearchNode.CardId(42), "id:42");
AssertParsesTo(new SearchNode.CardId(12), "id:\"12\"");
AssertParsesTo(new SearchNode.CardId(61), "id:#61");
}
[TestMethod]
public void ColonWithoutKeyPrefixIsFreeText() {
AssertParsesTo(new SearchNode.FreeText(":foo"), ":foo");
public void QuotedTextWithColonIsFreeText() {
AssertParsesTo(new SearchNode.FreeText("http://x"), "\"http://x\"");
}
[TestMethod]
public void WordMayContainHashAndPunctuation() {
AssertParsesTo(new SearchNode.FreeText("#61"), "#61");
AssertParsesTo(new SearchNode.FreeText("c#/.net"), "c#/.net");
}
#endregion
@@ -227,8 +234,84 @@ public class SearchQueryParserTests {
}
[TestMethod]
public void NonNumericIdIsAnError() {
AssertSyntaxError("id:12x");
[DataRow("tag: \"x\"")]
[DataRow("title: \"a b\"")]
public void SpacedQualifierPhraseValueIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
[DataRow("a\"b\"")]
[DataRow("\"a\"b")]
[DataRow("\"a\"\"b\"")]
[DataRow("tag:\"x\"y")]
[DataRow("tag:\"x\"\"y\"")]
public void PhraseGluedToOtherTextIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
[DataRow("\"\"")]
[DataRow("a \"\"")]
[DataRow("tag:\"\"")]
public void EmptyPhraseIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
public void PhraseMayTouchParentheses() {
AssertParsesTo(
new SearchNode.And([new SearchNode.FreeText("a b"), new SearchNode.FreeText("c")]),
"(\"a b\")(c)");
}
[TestMethod]
[DataRow("id:12x")]
[DataRow("id:abc")]
[DataRow("id:0")]
[DataRow("id:-1")]
[DataRow("id:+1")]
[DataRow("id:#")]
[DataRow("id:##1")]
[DataRow("id:1.5")]
[DataRow("id:٣")]
[DataRow("id:99999999999999999999")]
public void InvalidIdValueIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
public void QuotedIdValueIsAnError() {
AssertSyntaxError("id:\"61\"");
}
[TestMethod]
[DataRow("tag:AND")]
[DataRow("tag:OR")]
[DataRow("title:AND")]
public void BareOperatorAsQualifierValueIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
public void UnquotedQualifierValueWithColonIsAnError() {
AssertSyntaxError("tag:a:b");
}
[TestMethod]
[DataRow(":foo")]
[DataRow("http://x")]
[DataRow("a:")]
public void ColonAfterNonKeyIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
[DataRow("a\\b")]
[DataRow("\\")]
[DataRow("tag:a\\b")]
public void BackslashInBareWordIsAnError(string text) {
AssertSyntaxError(text);
}
[TestMethod]
+14 -5
View File
@@ -26,12 +26,14 @@ public class SearchSqlCompilerTests {
// Columns: 1 "To Do", 2 "In Progress".
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('To Do', '', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('In Progress', '', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('Archive', '', 1, 1);");
// Cards: 1 titled, 2 title-or-content matches, 3 untitled, 4 wildcard chars.
SqliteTestHelper.Exec(setup, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (1, 'Fix login', 'Cannot sign in', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (2, 'Green apple', 'the apple pie', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (2, '', 'banana split', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (1, '100% done', 'a_b test', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (3, 'ÄÖÜ Straße', '', 1, 1);");
// Tags: mixed case names plus one containing spaces.
SqliteTestHelper.Exec(setup, "INSERT INTO tags (name, color, description) VALUES ('Bug', '#ff0000', '');");
@@ -143,20 +145,27 @@ public class SearchSqlCompilerTests {
}
[TestMethod]
public void LikeWildcardsInUserTextMatchLiterally() {
public void PercentAndUnderscoreInUserTextMatchLiterally() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("100%"));
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("a_b"));
}
[TestMethod]
public void UnicodeCaseFoldingMatchesNonAscii() {
using var fixture = new SearchFixture();
// SQLite's LIKE would only fold ASCII; the ykb_fold function folds these too.
CollectionAssert.AreEqual(new[] { 5L }, fixture.Search("äöü"));
CollectionAssert.AreEqual(new[] { 5L }, fixture.Search("straße"));
}
[TestMethod]
public void CompiledPredicateUsesNamedParameters() {
CompiledSearch compiled = SearchSqlCompiler.Compile(new SearchNode.FreeText("x"));
StringAssert.Contains(compiled.Predicate, "$p0");
StringAssert.Contains(compiled.Predicate, "$p1");
Assert.AreEqual(2, compiled.Parameters.Count);
Assert.AreEqual("%x%", compiled.Parameters[0].Value);
Assert.AreEqual("%x%", compiled.Parameters[1].Value);
Assert.AreEqual(1, compiled.Parameters.Count);
Assert.AreEqual("x", compiled.Parameters[0].Value);
}
}