From 0e13f7a55726d550a8680e7ce485a57d8f043526 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Mon, 18 Mar 2024 23:12:58 -0500 Subject: [PATCH] Add date support --- .../Extensions/SqlNodeExtensions.cs | 19 ++-- .../SqlQueryParserTests.cs | 94 ++++++++++++------- 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs index 4a51ba36..095c14b8 100644 --- a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs +++ b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs @@ -138,14 +138,21 @@ public static string ToSqlString(this TermNode node, ISqlQueryVisitorContext con else builder.Append(" = "); - if (field != null && (field.IsNumber || field.IsBoolean)) - builder.Append(node.Term); - else - builder.Append("\"" + 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)) + builder.Append(term); + else if (field is { IsDate: true }) + builder.Append("DateTime.Parse(\"" + term + "\")"); + else + builder.Append("\"" + term + "\""); + } + public static string ToSqlString(this TermRangeNode node, ISqlQueryVisitorContext context) { if (String.IsNullOrEmpty(node.Field)) @@ -175,7 +182,7 @@ public static string ToSqlString(this TermRangeNode node, ISqlQueryVisitorContex { builder.Append(node.Field); builder.Append(node.MinInclusive == true ? " >= " : " > "); - builder.Append(node.Min); + AppendField(builder, field, node.Min); } if (node.Min != null && node.Max != null) @@ -185,7 +192,7 @@ public static string ToSqlString(this TermRangeNode node, ISqlQueryVisitorContex { builder.Append(node.Field); builder.Append(node.MaxInclusive == true ? " <= " : " < "); - builder.Append(node.Max); + AppendField(builder, field, node.Max); } if (node.Min != null && node.Max != null) diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs index fc802480..875a0d2c 100644 --- a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs @@ -60,27 +60,76 @@ public Task ValidQueries(string query, string expected) return ParseAndValidateQuery(query, expected, true); } + [Fact] + public async Task CanUseDateFilter() + { + var sp = GetServiceProvider(); + await using var db = await GetSampleContextWithDataAsync(sp); + var parser = sp.GetRequiredService(); + + var context = parser.GetContext(db.Employees.EntityType); + context.Fields.Add(new EntityFieldInfo { Field = "age", IsNumber = true, Data = {{ "DataDefinitionId", 1 }}}); + context.ValidationOptions.AllowedFields.Add("age"); + + string sqlExpected = db.Employees.Where(e => e.Created > new DateTime(2024, 1, 1)).ToQueryString(); + string sqlActual = db.Employees.Where("""created > DateTime.Parse("2024-01-01")""").ToQueryString(); + Assert.Equal(sqlExpected, sqlActual); + string sql = await parser.ToSqlAsync("created:>2024-01-01", context); + sqlActual = db.Employees.Where(sql).ToQueryString(); + Assert.Equal(sqlExpected, sqlActual); + } + [Fact] public async Task CanGenerateSql() + { + var sp = GetServiceProvider(); + await using var db = await GetSampleContextWithDataAsync(sp); + var parser = sp.GetRequiredService(); + + var context = parser.GetContext(db.Employees.EntityType); + 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(); + Assert.Equal(sqlExpected, sqlActual); + string sql = await parser.ToSqlAsync("company.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); + sqlActual = q.Where(sql, db.Employees).ToQueryString(); + Assert.Equal(sqlExpected, sqlActual); + + await Assert.ThrowsAsync(() => parser.ToSqlAsync("company.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(); + + Assert.Single(employees); + var employee = employees.Single(); + Assert.Equal("John Doe", employee.FullName); + } + + public IServiceProvider GetServiceProvider() { var services = new ServiceCollection(); services.AddDbContext((_, x) => { - x.UseSqlServer("Server=localhost;User Id=sa;Password=P@ssword1;Timeout=5;Initial Catalog=foundatio;Encrypt=False", o => o.MigrationsAssembly("ConcordServicing.Web")); + x.UseSqlServer("Server=localhost;User Id=sa;Password=P@ssword1;Timeout=5;Initial Catalog=foundatio;Encrypt=False"); }, ServiceLifetime.Scoped, ServiceLifetime.Singleton); var parser = new SqlQueryParser(); + parser.Configuration.UseEntityTypePropertyFilter(p => p.Name != nameof(Company.Description)); parser.Configuration.AddQueryVisitor(new DynamicFieldVisitor()); - parser.Configuration.UseEntityTypePropertyFilter(p => - { - if (p.DeclaringType.ClrType == typeof(Company) && p.Name == "Description") - return false; - - return true; - }); services.AddSingleton(parser); - var sp = services.BuildServiceProvider(); + return services.BuildServiceProvider(); + } - await using var db = sp.GetRequiredService(); + public async Task GetSampleContextWithDataAsync(IServiceProvider sp) + { + var db = sp.GetRequiredService(); + var parser = sp.GetRequiredService(); var dbParser = db.GetService(); Assert.Same(parser, dbParser); @@ -111,30 +160,7 @@ public async Task CanGenerateSql() }); await db.SaveChangesAsync(); - var context = parser.GetContext(db.Employees.EntityType); - 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(); - Assert.Equal(sqlExpected, sqlActual); - string sql = await parser.ToSqlAsync("company.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); - sqlActual = q.Where(sql, db.Employees).ToQueryString(); - Assert.Equal(sqlExpected, sqlActual); - - await Assert.ThrowsAsync(() => parser.ToSqlAsync("company.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(); - - Assert.Single(employees); - var employee = employees.Single(); - Assert.Equal("John Doe", employee.FullName); + return db; } private async Task ParseAndValidateQuery(string query, string expected, bool isValid)