Skip to content

Commit

Permalink
Adding support for junction tables. Better recursion detection.
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith committed Mar 19, 2024
1 parent 1c94c63 commit 272267b
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 31 deletions.
37 changes: 31 additions & 6 deletions src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,45 @@ public static string ToSqlString(this TermNode node, ISqlQueryVisitorContext con
if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append("NOT ");

builder.Append(node.Field);
if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append(" != ");
if (field.IsCollection)
{
var index = node.Field.LastIndexOf('.');
var collectionField = node.Field.Substring(0, index);
var 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(")");
}
else
builder.Append(" = ");
{
builder.Append(node.Field);
if (node.IsNegated.HasValue && node.IsNegated.Value)
builder.Append(" != ");
else
builder.Append(" = ");

AppendField(builder, field, node.Term);
AppendField(builder, field, node.Term);
}

return builder.ToString();
}

private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term)
{
if (field != null && (field.IsNumber || field.IsBoolean))
if (field == null)
return;

if (field.IsNumber || field.IsBoolean)
builder.Append(term);
else if (field is { IsDate: true })
builder.Append("DateTime.Parse(\"" + term + "\")");
Expand Down
53 changes: 38 additions & 15 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,35 +94,58 @@ public SqlQueryVisitorContext GetContext(IEntityType entityType)
};
}

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

if (depth > 0 && entityTypeStack.Contains(entityType))
return;

prefix ??= "";
entityTypeStack.Push(entityType);

if (depth > Configuration.MaxFieldDepth)
return;

visited.Add(entityType);
prefix ??= "";

foreach (var property in entityType.GetProperties())
{
if (Configuration.EntityTypePropertyFilter(property))
fields.Add(new EntityFieldInfo
{
Field = prefix + property.Name,
IsNumber = property.ClrType.UnwrapNullable().IsNumeric(),
IsDate = property.ClrType.UnwrapNullable().IsDateTime(),
IsBoolean = property.ClrType.UnwrapNullable().IsBoolean()
});
string propertyPath = prefix + property.Name;
if (!Configuration.EntityTypePropertyFilter(property))
continue;

fields.Add(new EntityFieldInfo
{
Field = propertyPath,
IsNumber = property.ClrType.UnwrapNullable().IsNumeric(),
IsDate = property.ClrType.UnwrapNullable().IsDateTime(),
IsBoolean = property.ClrType.UnwrapNullable().IsBoolean(),
IsCollection = isCollection
});
}

if (isCollection)
return;

foreach (var nav in entityType.GetNavigations())
{
if (visited.Contains(nav.TargetEntityType) || !Configuration.EntityTypeNavigationFilter(nav))
string propertyPath = prefix + nav.Name;
if (!Configuration.EntityTypeNavigationFilter(nav))
continue;

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

foreach (var skipNav in entityType.GetSkipNavigations())
{
string propertyPath = prefix + skipNav.Name;
if (!Configuration.EntityTypeSkipNavigationFilter(skipNav))
continue;

AddEntityFields(fields, nav.TargetEntityType, visited, prefix + nav.Name + ".");
AddEntityFields(fields, skipNav.TargetEntityType, entityTypeStack, propertyPath + ".", true, depth + 1);
}

entityTypeStack.Pop();
}

private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
Expand Down
13 changes: 13 additions & 0 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ public SqlQueryParserConfiguration() {
public ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
public string[] DefaultFields { get; private set; }

public int MaxFieldDepth { get; private set; } = 10;
public QueryFieldResolver FieldResolver { get; private set; }
public EntityTypePropertyFilter EntityTypePropertyFilter { get; private set; } = static _ => true;
public EntityTypeNavigationFilter EntityTypeNavigationFilter { get; private set; } = static _ => true;
public EntityTypeSkipNavigationFilter EntityTypeSkipNavigationFilter { get; private set; } = static _ => true;
public IncludeResolver IncludeResolver { get; private set; }
public QueryValidationOptions ValidationOptions { get; private set; }
public ChainedQueryVisitor SortVisitor { get; } = new();
Expand All @@ -42,6 +44,11 @@ public SqlQueryParserConfiguration SetDefaultFields(string[] fields) {
return this;
}

public SqlQueryParserConfiguration SetFieldDepth(int maxFieldDepth) {
MaxFieldDepth = maxFieldDepth;
return this;
}

public SqlQueryParserConfiguration UseEntityTypePropertyFilter(EntityTypePropertyFilter filter) {
EntityTypePropertyFilter = filter;
return this;
Expand All @@ -52,6 +59,11 @@ public SqlQueryParserConfiguration UseEntityTypeNavigationFilter(EntityTypeNavig
return this;
}

public SqlQueryParserConfiguration UseEntityTypeSkipNavigationFilter(EntityTypeSkipNavigationFilter filter) {
EntityTypeSkipNavigationFilter = filter;
return this;
}

public SqlQueryParserConfiguration UseFieldResolver(QueryFieldResolver resolver, int priority = 10) {
FieldResolver = resolver;
ReplaceVisitor<FieldResolverQueryVisitor>(new FieldResolverQueryVisitor(resolver), priority);
Expand Down Expand Up @@ -233,4 +245,5 @@ public SqlQueryParserConfiguration AddAggregationVisitorAfter<T>(IChainableQuery
}

public delegate bool EntityTypeNavigationFilter(INavigation navigation);
public delegate bool EntityTypeSkipNavigationFilter(ISkipNavigation navigation);
public delegate bool EntityTypePropertyFilter(IProperty property);
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ public class SqlQueryVisitorContext : QueryVisitorContext, ISqlQueryVisitorConte
public IEntityType EntityType { get; set; }
}

[DebuggerDisplay("{Field} IsNumber: {IsNumber} IsDate: {IsDate} IsBoolean: {IsBoolean} Children: {Children?.Count}")]
[DebuggerDisplay("{Field} IsNumber: {IsNumber} IsDate: {IsDate} IsBoolean: {IsBoolean} IsCollection: {IsCollection}")]
public class EntityFieldInfo
{
public string Field { get; set; }
public bool IsNumber { get; set; }
public bool IsDate { get; set; }
public bool IsBoolean { get; set; }
public bool IsCollection { get; set; }
public IDictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
}
3 changes: 1 addition & 2 deletions tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public class Employee {
public int Id { get; set; }
public string FullName { get; set; }
public string Title { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; }
public List<Company> Companies { get; set; }
public List<DataValue> DataValues { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
}
Expand Down
14 changes: 7 additions & 7 deletions tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,19 @@ public async Task CanGenerateSql()
context.Fields.Add(new EntityFieldInfo { Field = "age", IsNumber = true, Data = {{ "DataDefinitionId", 1 }}});
context.ValidationOptions.AllowedFields.Add("age");

string sqlExpected = db.Employees.Where(e => e.Company.Name == "acme" && e.DataValues.Any(dv => dv.DataDefinitionId == 1 && dv.NumberValue == 30)).ToQueryString();
string sqlActual = db.Employees.Where("""company.name = "acme" AND DataValues.Any(DataDefinitionId = 1 AND NumberValue = 30) """).ToQueryString();
string sqlExpected = db.Employees.Where(e => e.Companies.Any(c => c.Name == "acme") && e.DataValues.Any(dv => dv.DataDefinitionId == 1 && dv.NumberValue == 30)).ToQueryString();
string sqlActual = db.Employees.Where("""Companies.Any(Name = "acme") AND DataValues.Any(DataDefinitionId = 1 AND NumberValue = 30) """).ToQueryString();
Assert.Equal(sqlExpected, sqlActual);
string sql = await parser.ToSqlAsync("company.name:acme age:30", context);
string sql = await parser.ToSqlAsync("companies.name:acme age:30", context);
sqlActual = db.Employees.Where(sql).ToQueryString();
Assert.Equal(sqlExpected, sqlActual);

var q = db.Employees.AsNoTracking();
sql = await parser.ToSqlAsync("company.name:acme age:30", context);
sql = await parser.ToSqlAsync("companies.name:acme age:30", context);
sqlActual = q.Where(sql, db.Employees).ToQueryString();
Assert.Equal(sqlExpected, sqlActual);

await Assert.ThrowsAsync<ValidationException>(() => parser.ToSqlAsync("company.description:acme", context));
await Assert.ThrowsAsync<ValidationException>(() => parser.ToSqlAsync("companies.description:acme", context));

var employees = await db.Employees.Where(e => e.Title == "software developer" && e.DataValues.Any(dv => dv.DataDefinitionId == 1 && dv.NumberValue == 30))
.ToListAsync();
Expand Down Expand Up @@ -165,14 +165,14 @@ public async Task<SampleContext> GetSampleContextWithDataAsync(IServiceProvider
FullName = "John Doe",
Title = "Software Developer",
DataValues = [ new() { Definition = company.DataDefinitions[0], NumberValue = 30 } ],
Company = company
Companies = [company]
});
db.Employees.Add(new Employee
{
FullName = "Jane Doe",
Title = "Software Developer",
DataValues = [ new() { Definition = company.DataDefinitions[0], NumberValue = 23 } ],
Company = company
Companies = [company]
});
await db.SaveChangesAsync();

Expand Down

0 comments on commit 272267b

Please sign in to comment.