Skip to content

Commit

Permalink
Search values (#89)
Browse files Browse the repository at this point in the history
* Adding default field search tokenizer and sample

* More changes

* A different approach

* Fix tests

* Fix tests again

* Rename Tokenizer to SearchTokenizer
  • Loading branch information
ejsmith authored Oct 21, 2024
1 parent d00f61e commit 69ffc9c
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 85 deletions.
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ services:
- "1433:1433" # login with sa:P@ssword1
environment:
- "ACCEPT_EULA=Y"
- "SA_PASSWORD=P@ssword1"
- "MSSQL_SA_PASSWORD=P@ssword1"
- "MSSQL_PID=Developer"
user: root
networks:
- foundatio
healthcheck:
test:
[
Expand All @@ -49,8 +52,6 @@ services:
]
interval: 1s
retries: 20
networks:
- foundatio

ready:
image: andrewlock/wait-for-dependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;

namespace Foundatio.Parsers.SqlQueries.Extensions;

internal static class EnumerableExtensions {
public delegate void ElementAction<in T>(T element, ElementInfo info);

public static void ForEach<T>(this IEnumerable<T> elements, ElementAction<T> action)
{
using IEnumerator<T> enumerator = elements.GetEnumerator();
bool isFirst = true;
bool hasNext = enumerator.MoveNext();
int index = 0;

while (hasNext)
{
T current = enumerator.Current;
hasNext = enumerator.MoveNext();
action(current, new ElementInfo(index, isFirst, !hasNext));
isFirst = false;
index++;
}
}

public struct ElementInfo {
public ElementInfo(int index, bool isFirst, bool isLast)
: this() {
Index = index;
IsFirst = isFirst;
IsLast = isLast;
}

public int Index { get; private set; }
public bool IsFirst { get; private set; }
public bool IsLast { get; private set; }
}
}

224 changes: 149 additions & 75 deletions src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,15 @@ public static string ToDynamicLinqString(this MissingNode node, ISqlQueryVisitor
return builder.ToString();
}

public static EntityFieldInfo GetFieldInfo(List<EntityFieldInfo> fields, string field)
{
if (fields == null)
return new EntityFieldInfo { Field = field };

return fields.FirstOrDefault(f => f.Field.Equals(field, StringComparison.OrdinalIgnoreCase)) ??
new EntityFieldInfo { Field = field };
}

public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorContext context)
{
if (!String.IsNullOrEmpty(node.Prefix))
context.AddValidationError("Prefix is not supported for term range queries.");

// support overriding the generated query
if (node.TryGetQuery(out string query))
return query;

var builder = new StringBuilder();

if (String.IsNullOrEmpty(node.Field))
Expand All @@ -128,39 +123,109 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
return String.Empty;
}

for (int index = 0; index < context.DefaultFields.Length; index++)
var fieldTerms = new Dictionary<EntityFieldInfo, SearchTerm>();
foreach (string df in context.DefaultFields)
{
var fieldInfo = GetFieldInfo(context.Fields, df);
if (!fieldTerms.TryGetValue(fieldInfo, out var searchTerm))
{
searchTerm = new SearchTerm
{
FieldInfo = fieldInfo,
Term = node.Term,
Operator = SqlSearchOperator.StartsWith
};
fieldTerms[fieldInfo] = searchTerm;
}

context.SearchTokenizer.Invoke(searchTerm);
}

fieldTerms.ForEach((kvp, x) =>
{
builder.Append(index == 0 ? "(" : " OR ");
builder.Append(x.IsFirst ? "(" : " OR ");
var searchTerm = kvp.Value;
var tokens = kvp.Value.Tokens ?? [kvp.Value.Term];

var defaultField = GetFieldInfo(context.Fields, context.DefaultFields[index]);
if (defaultField.IsCollection)
if (searchTerm.FieldInfo.IsCollection)
{
int dotIndex = defaultField.Field.LastIndexOf('.');
string collectionField = defaultField.Field.Substring(0, dotIndex);
string fieldName = defaultField.Field.Substring(dotIndex + 1);

builder.Append(collectionField);
builder.Append(".Any(");
builder.Append(fieldName);
builder.Append(".Contains(\"").Append(node.Term).Append("\")");
builder.Append(")");
int dotIndex = searchTerm.FieldInfo.Field.LastIndexOf('.');
string collectionField = searchTerm.FieldInfo.Field.Substring(0, dotIndex);
string fieldName = searchTerm.FieldInfo.Field.Substring(dotIndex + 1);

if (searchTerm.Operator == SqlSearchOperator.Equals)
{
builder.Append(collectionField);
builder.Append(".Any(");
builder.Append(fieldName);
builder.Append(" in (");
builder.Append(String.Join(',', tokens.Select(t => "\"" + t + "\"")));
builder.Append("))");
}
else if (searchTerm.Operator == SqlSearchOperator.Contains)
{
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(collectionField);
builder.Append(".Any(");
builder.Append(fieldName);
builder.Append(".Contains(\"");
builder.Append(token);
builder.Append("\"))");
if (i.IsLast)
builder.Append(")");
});
}
else if (searchTerm.Operator == SqlSearchOperator.StartsWith)
{
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(collectionField);
builder.Append(".Any(");
builder.Append(fieldName);
builder.Append(".StartsWith(\"");
builder.Append(token);
builder.Append("\"))");
if (i.IsLast)
builder.Append(")");
});
}
}
else
{
builder.Append(defaultField.Field).Append(".Contains(\"").Append(node.Term).Append("\")");
if (searchTerm.Operator == SqlSearchOperator.Equals)
{
builder.Append(searchTerm.FieldInfo.Field).Append(" in (");
builder.Append(String.Join(',', tokens.Select(t => "\"" + t + "\"")));
builder.Append(")");
}
else if (searchTerm.Operator == SqlSearchOperator.Contains)
{
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(searchTerm.FieldInfo.Field).Append(".Contains(\"").Append(token).Append("\")");
if (i.IsLast)
builder.Append(")");
});
}
else if (searchTerm.Operator == SqlSearchOperator.StartsWith)
{
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(searchTerm.FieldInfo.Field).Append(".StartsWith(\"").Append(token).Append("\")");
if (i.IsLast)
builder.Append(")");
});
}
}

if (index == context.DefaultFields.Length - 1)
if (x.IsLast)
builder.Append(")");
}
});

return builder.ToString();
}

// support overriding the generated query
if (node.TryGetQuery(out string query))
return query;

var field = GetFieldInfo(context.Fields, node.Field);

if (node.IsNegated.HasValue && node.IsNegated.Value)
Expand Down Expand Up @@ -199,52 +264,6 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
return builder.ToString();
}

private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term)
{
if (field == null)
return;

if (field.IsNumber || field.IsBoolean || field.IsMoney)
{
builder.Append(term);
}
else if (field is { IsDate: true })
{
term = term.Trim();
if (term.StartsWith("now", StringComparison.OrdinalIgnoreCase))
{
builder.Append("DateTime.UtcNow");

if (term.Length == 3)
return;

builder.Append(".");

string method = term[^1..] switch
{
"y" => "AddYears",
"M" => "AddMonths",
"d" => "AddDays",
"h" => "AddHours",
"H" => "AddHours",
"m" => "AddMinutes",
"s" => "AddSeconds",
_ => throw new NotSupportedException("Invalid date operation.")
};

bool subtract = term.Substring(3, 1) == "-";

builder.Append(method).Append("(").Append(subtract ? "-" : "").Append(term.Substring(4, term.Length - 5)).Append(")");
}
else
{
builder.Append("DateTime.Parse(\"" + term + "\")");
}
}
else
builder.Append("\"" + term + "\"");
}

public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisitorContext context)
{
if (String.IsNullOrEmpty(node.Field))
Expand Down Expand Up @@ -306,6 +325,61 @@ public static string ToDynamicLinqString(this IQueryNode node, ISqlQueryVisitorC
};
}

public static EntityFieldInfo GetFieldInfo(List<EntityFieldInfo> fields, string field)
{
if (fields == null)
return new EntityFieldInfo { Field = field };

return fields.FirstOrDefault(f => f.Field.Equals(field, StringComparison.OrdinalIgnoreCase)) ??
new EntityFieldInfo { Field = field };
}

private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term)
{
if (field == null)
return;

if (field.IsNumber || field.IsBoolean || field.IsMoney)
{
builder.Append(term);
}
else if (field is { IsDate: true })
{
term = term.Trim();
if (term.StartsWith("now", StringComparison.OrdinalIgnoreCase))
{
builder.Append("DateTime.UtcNow");

if (term.Length == 3)
return;

builder.Append(".");

string method = term[^1..] switch
{
"y" => "AddYears",
"M" => "AddMonths",
"d" => "AddDays",
"h" => "AddHours",
"H" => "AddHours",
"m" => "AddMinutes",
"s" => "AddSeconds",
_ => throw new NotSupportedException("Invalid date operation.")
};

bool subtract = term.Substring(3, 1) == "-";

builder.Append(method).Append("(").Append(subtract ? "-" : "").Append(term.Substring(4, term.Length - 5)).Append(")");
}
else
{
builder.Append("DateTime.Parse(\"" + term + "\")");
}
}
else
builder.Append("\"" + term + "\"");
}

private const string QueryKey = "Query";
public static void SetQuery(this IQueryNode node, string query)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,10 @@ private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
if (Configuration.IncludeResolver != null && context.GetIncludeResolver() == null)
context.SetIncludeResolver(Configuration.IncludeResolver);
}

if (context is ISqlQueryVisitorContext sqlContext)
{
sqlContext.SearchTokenizer = Configuration.SearchTokenizer;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Foundatio.Parsers.LuceneQueries;
using Foundatio.Parsers.LuceneQueries.Visitors;
using Foundatio.Parsers.SqlQueries.Visitors;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -25,6 +26,7 @@ public SqlQueryParserConfiguration()

public int MaxFieldDepth { get; private set; } = 10;
public QueryFieldResolver FieldResolver { get; private set; }
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
public EntityTypePropertyFilter EntityTypePropertyFilter { get; private set; } = static _ => true;
public EntityTypeNavigationFilter EntityTypeNavigationFilter { get; private set; } = static _ => true;
public EntityTypeSkipNavigationFilter EntityTypeSkipNavigationFilter { get; private set; } = static _ => true;
Expand All @@ -48,6 +50,12 @@ public SqlQueryParserConfiguration SetDefaultFields(string[] fields)
return this;
}

public SqlQueryParserConfiguration SetSearchTokenizer(Action<SearchTerm> tokenizer)
{
SearchTokenizer = tokenizer;
return this;
}

public SqlQueryParserConfiguration SetFieldDepth(int maxFieldDepth)
{
MaxFieldDepth = maxFieldDepth;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Foundatio.Parsers.LuceneQueries.Visitors;

namespace Foundatio.Parsers.SqlQueries.Visitors;

public interface ISqlQueryVisitorContext : IQueryVisitorContext
{
List<EntityFieldInfo> Fields { get; set; }
Action<SearchTerm> SearchTokenizer { get; set; }
}
Loading

0 comments on commit 69ffc9c

Please sign in to comment.