Skip to content

Commit

Permalink
More SQL search improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith committed Oct 22, 2024
1 parent 69ffc9c commit fd1859c
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 114 deletions.
191 changes: 91 additions & 100 deletions src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Foundatio.Parsers.LuceneQueries.Extensions;
using Foundatio.Parsers.LuceneQueries.Nodes;
using Foundatio.Parsers.SqlQueries.Visitors;
using Microsoft.Extensions.Primitives;

namespace Foundatio.Parsers.SqlQueries.Extensions;

Expand Down Expand Up @@ -70,13 +71,18 @@ public static string ToDynamicLinqString(this ExistsNode node, ISqlQueryVisitorC
if (node.TryGetQuery(out string query))
return query;

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

var builder = new StringBuilder();

builder.Append(node.Field);
builder.Append(fieldPrefix);
builder.Append(field.Name);
if (!node.IsNegated.HasValue || !node.IsNegated.Value)
builder.Append(" != null");
else
builder.Append(" == null");
builder.Append(fieldSuffix);

return builder.ToString();
}
Expand All @@ -93,13 +99,17 @@ public static string ToDynamicLinqString(this MissingNode node, ISqlQueryVisitor
if (node.TryGetQuery(out string query))
return query;

var builder = new StringBuilder();
var field = GetFieldInfo(context.Fields, node.Field);
var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();

builder.Append(node.Field);
var builder = new StringBuilder();
builder.Append(fieldPrefix);
builder.Append(field.Name);
if (!node.IsNegated.HasValue || !node.IsNegated.Value)
builder.Append(" == null");
else
builder.Append(" != null");
builder.Append(fieldSuffix);

return builder.ToString();
}
Expand Down Expand Up @@ -143,80 +153,51 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon

fieldTerms.ForEach((kvp, x) =>
{
if (x.IsFirst && node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append("!");

builder.Append(x.IsFirst ? "(" : " OR ");

var searchTerm = kvp.Value;
var tokens = kvp.Value.Tokens ?? [kvp.Value.Term];
var (fieldPrefix, fieldSuffix) = kvp.Key.GetFieldPrefixAndSuffix();

if (searchTerm.FieldInfo.IsCollection)
if (searchTerm.Operator == SqlSearchOperator.Equals)
{
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(")");
});
}
builder.Append(fieldPrefix);
builder.Append(kvp.Key.Name);
builder.Append(" in (");
builder.Append(String.Join(',', tokens.Select(t => "\"" + t + "\"")));
builder.Append(")");
builder.Append(fieldSuffix);
}
else
else if (searchTerm.Operator == SqlSearchOperator.Contains)
{
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(")");
});
}
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(fieldPrefix);
builder.Append(kvp.Key.Name);
builder.Append(".Contains(\"");
builder.Append(token);
builder.Append("\")");
builder.Append(fieldSuffix);
if (i.IsLast)
builder.Append(")");
});
}
else if (searchTerm.Operator == SqlSearchOperator.StartsWith)
{
tokens.ForEach((token, i) => {
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(fieldPrefix);
builder.Append(kvp.Key.Name);
builder.Append(".StartsWith(\"");
builder.Append(token);
builder.Append("\")");
builder.Append(fieldSuffix);
if (i.IsLast)
builder.Append(")");
});
}

if (x.IsLast)
Expand All @@ -227,38 +208,42 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
}

var field = GetFieldInfo(context.Fields, node.Field);
var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();
var searchOperator = SqlSearchOperator.Equals;
if (node.Term.StartsWith("*") && node.Term.EndsWith("*"))
searchOperator = SqlSearchOperator.Contains;
else if (node.Term.EndsWith("*"))
searchOperator = SqlSearchOperator.StartsWith;

if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append("!");

if (field.IsCollection)
if (searchOperator == SqlSearchOperator.Equals)
{
int index = node.Field.LastIndexOf('.');
string collectionField = node.Field.Substring(0, index);
string fieldName = node.Field.Substring(index + 1);

builder.Append(collectionField);
builder.Append(".Any(");
builder.Append(fieldName);

if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append(" != ");
else
builder.Append(" = ");

AppendField(builder, field, node.Term);

builder.Append(")");
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(" = \"");
builder.Append(node.Term);
builder.Append("\"");
builder.Append(fieldSuffix);
}
else if (searchOperator == SqlSearchOperator.Contains)
{
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(".Contains(\"");
builder.Append(node.Term);
builder.Append("\")");
builder.Append(fieldSuffix);
}
else
{
builder.Append(node.Field);
if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append(" != ");
else
builder.Append(" = ");

AppendField(builder, field, node.Term);
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(".StartsWith(\"");
builder.Append(node.Term);
builder.Append("\")");
builder.Append(fieldSuffix);
}

return builder.ToString();
Expand All @@ -281,29 +266,35 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
if (!field.IsNumber && !field.IsDate && !field.IsMoney)
context.AddValidationError("Field must be a number, money or date for term range queries.");

var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();

var builder = new StringBuilder();

if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append("NOT ");
builder.Append("!");

if (node.Min != null && node.Max != null)
builder.Append("(");

if (node.Min != null)
{
builder.Append(node.Field);
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(node.MinInclusive == true ? " >= " : " > ");
AppendField(builder, field, node.Min);
builder.Append(fieldSuffix);
}

if (node.Min != null && node.Max != null)
builder.Append(" AND ");

if (node.Max != null)
{
builder.Append(node.Field);
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(node.MaxInclusive == true ? " <= " : " < ");
AppendField(builder, field, node.Max);
builder.Append(fieldSuffix);
}

if (node.Min != null && node.Max != null)
Expand All @@ -328,10 +319,10 @@ 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 new EntityFieldInfo { Name = field, FullName = field};

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

private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term)
Expand Down
35 changes: 28 additions & 7 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ public SqlQueryVisitorContext GetContext(IEntityType entityType)
if (!_entityFieldCache.TryGetValue(entityType, out var fields))
{
fields = new List<EntityFieldInfo>();
AddEntityFields(fields, entityType);
AddEntityFields(fields, null, entityType);
_entityFieldCache.TryAdd(entityType, fields);
}

// make copy of fields list to avoid modifying the cached list
fields = fields.ToList();

var validationOptions = new QueryValidationOptions();
foreach (string field in fields.Select(f => f.Field))
foreach (string field in fields.Where(f => !f.IsNavigation).Select(f => f.FullName))
validationOptions.AllowedFields.Add(field);

Configuration.SetValidationOptions(validationOptions);
Expand All @@ -101,7 +101,7 @@ public SqlQueryVisitorContext GetContext(IEntityType entityType)
};
}

private void AddEntityFields(List<EntityFieldInfo> fields, IEntityType entityType, Stack<IEntityType> entityTypeStack = null, string prefix = null, bool isCollection = false, int depth = 0)
private void AddEntityFields(List<EntityFieldInfo> fields, EntityFieldInfo parent, IEntityType entityType, Stack<IEntityType> entityTypeStack = null, string prefix = null, int depth = 0)
{
entityTypeStack ??= new Stack<IEntityType>();

Expand All @@ -123,11 +123,12 @@ private void AddEntityFields(List<EntityFieldInfo> fields, IEntityType entityTyp
string propertyPath = prefix + property.Name;
fields.Add(new EntityFieldInfo
{
Field = propertyPath,
Name = property.Name,
FullName = propertyPath,
IsNumber = property.ClrType.UnwrapNullable().IsNumeric(),
IsDate = property.ClrType.UnwrapNullable().IsDateTime(),
IsBoolean = property.ClrType.UnwrapNullable().IsBoolean(),
IsCollection = isCollection
Parent = parent
});
}

Expand All @@ -139,7 +140,17 @@ private void AddEntityFields(List<EntityFieldInfo> fields, IEntityType entityTyp
string propertyPath = prefix + nav.Name;
bool isNavCollection = nav is IReadOnlyNavigationBase { IsCollection: true };

AddEntityFields(fields, nav.TargetEntityType, entityTypeStack, propertyPath + ".", isNavCollection, depth + 1);
var navFieldInfo = new EntityFieldInfo
{
IsCollection = isNavCollection,
IsNavigation = true,
Name = nav.Name,
FullName = propertyPath,
Parent = parent
};
fields.Add(navFieldInfo);

AddEntityFields(fields, navFieldInfo, nav.TargetEntityType, entityTypeStack, propertyPath + ".", depth + 1);
}

foreach (var skipNav in entityType.GetSkipNavigations())
Expand All @@ -149,7 +160,17 @@ private void AddEntityFields(List<EntityFieldInfo> fields, IEntityType entityTyp

string propertyPath = prefix + skipNav.Name;

AddEntityFields(fields, skipNav.TargetEntityType, entityTypeStack, propertyPath + ".", skipNav.IsCollection, depth + 1);
var navFieldInfo = new EntityFieldInfo
{
IsCollection = skipNav.IsCollection,
IsNavigation = true,
Name = skipNav.Name,
FullName = propertyPath,
Parent = parent
};
fields.Add(navFieldInfo);

AddEntityFields(fields, navFieldInfo, skipNav.TargetEntityType, entityTypeStack, propertyPath + ".", depth + 1);
}

entityTypeStack.Pop();
Expand Down
Loading

0 comments on commit fd1859c

Please sign in to comment.