From 1de92acbf9198d39c5929e12232544f5e6559d22 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 13 Mar 2024 13:01:40 -0500 Subject: [PATCH] Progress --- docker-compose.yml | 21 +++++ .../Extensions/QueryableExtensions.cs | 9 +++ .../Extensions/SqlNodeExtensions.cs | 80 ++++++++++++++++++- .../Visitors/SqlQueryVisitorContext.cs | 1 + .../SampleContext.cs | 3 +- .../SqlQueryParserTests.cs | 8 +- 6 files changed, 112 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 86067c01..eb818b54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,27 @@ services: networks: - foundatio + sqlserver: + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - "1433:1433" # login with sa:P@ssword1 + environment: + - "ACCEPT_EULA=Y" + - "SA_PASSWORD=P@ssword1" + - "MSSQL_PID=Developer" + healthcheck: + test: + [ + "CMD", + "/opt/mssql-tools/bin/sqlcmd", + "-Usa", + "-PP@ssword1", + "-Q", + "select 1", + ] + interval: 1s + retries: 20 + ready: image: andrewlock/wait-for-dependencies command: elasticsearch:9200 diff --git a/src/Foundatio.Parsers.SqlQueries/Extensions/QueryableExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Extensions/QueryableExtensions.cs index 7f06e84a..79ecb497 100644 --- a/src/Foundatio.Parsers.SqlQueries/Extensions/QueryableExtensions.cs +++ b/src/Foundatio.Parsers.SqlQueries/Extensions/QueryableExtensions.cs @@ -34,6 +34,15 @@ public static IQueryable LuceneWhere(this IQueryable source, string que { var fields = new List(); AddFields(fields, entityType); + + // lookup and add custom fields + fields.Add(new FieldInfo + { + Field = "age", + Data = {{ "DataDefinitionId", 1 }}, + IsNumber = true + }); + return fields; }); diff --git a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs index f0971b72..b6a986cc 100644 --- a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs +++ b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs @@ -10,6 +10,10 @@ public static class SqlNodeExtensions { public static string ToSqlString(this GroupNode node, ISqlQueryVisitorContext context) { + // support overriding the generated query + if (node.TryGetQuery(out string query)) + return query; + if (node.Left == null && node.Right == null) return String.Empty; @@ -60,6 +64,10 @@ public static string ToSqlString(this ExistsNode node, ISqlQueryVisitorContext c if (String.IsNullOrEmpty(node.Field)) throw new ArgumentException("Field is required for exists node queries."); + // support overriding the generated query + if (node.TryGetQuery(out string query)) + return query; + var builder = new StringBuilder(); if (node.IsNegated.HasValue && node.IsNegated.Value) @@ -79,6 +87,10 @@ public static string ToSqlString(this MissingNode node, ISqlQueryVisitorContext if (!String.IsNullOrEmpty(node.Prefix)) throw new ArgumentException("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 (node.IsNegated.HasValue && node.IsNegated.Value) @@ -98,6 +110,46 @@ public static string ToSqlString(this TermNode node, ISqlQueryVisitorContext con if (!String.IsNullOrEmpty(node.Prefix)) throw new ArgumentException("Prefix is not supported for term range queries."); + // TODO: This needs to resolve the field recursively + var field = context.Fields.FirstOrDefault(f => f.Field.Equals(node.Field, StringComparison.OrdinalIgnoreCase)); + + // TODO: Remove this hard coded + if (field != null && field.Data.TryGetValue("DataDefinitionId", out object value) && value is int dataDefinitionId) + { + var customFieldBuilder = new StringBuilder(); + + customFieldBuilder.Append("DataValues.Any(DataDefinitionId = "); + customFieldBuilder.Append(dataDefinitionId); + customFieldBuilder.Append(" AND "); + if (field is { IsNumber: true }) + customFieldBuilder.Append("NumberValue"); + else if (field is { IsBoolean: true }) + customFieldBuilder.Append("BooleanValue"); + else if (field is { IsDate: true }) + customFieldBuilder.Append("DateValue"); + else + customFieldBuilder.Append("StringValue"); + + customFieldBuilder.Append(" = "); + if (field is { IsNumber: true } or { IsBoolean: true }) + { + customFieldBuilder.Append(node.Term); + } + else + { + customFieldBuilder.Append("\""); + customFieldBuilder.Append(node.Term); + customFieldBuilder.Append("\""); + } + customFieldBuilder.Append(")"); + + node.SetQuery(customFieldBuilder.ToString()); + } + + // support overriding the generated query + if (node.TryGetQuery(out string query)) + return query; + var builder = new StringBuilder(); if (node.IsNegated.HasValue && node.IsNegated.Value) @@ -109,8 +161,6 @@ public static string ToSqlString(this TermNode node, ISqlQueryVisitorContext con else builder.Append(" = "); - // TODO: This needs to resolve the field recursively - var field = context.Fields.FirstOrDefault(f => f.Field.Equals(node.Field, StringComparison.OrdinalIgnoreCase)); if (field != null && (field.IsNumber || field.IsBoolean)) builder.Append(node.Term); else @@ -128,6 +178,10 @@ public static string ToSqlString(this TermRangeNode node, ISqlQueryVisitorContex if (!String.IsNullOrEmpty(node.Proximity)) throw new ArgumentException("Proximity 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 (node.IsNegated.HasValue && node.IsNegated.Value) @@ -171,4 +225,26 @@ public static string ToSqlString(this IQueryNode node, ISqlQueryVisitorContext c _ => throw new NotSupportedException($"Node type {node.GetType().Name} is not supported.") }; } + + private const string QueryKey = "Query"; + public static void SetQuery(this IQueryNode node, string query) + { + node.Data[QueryKey] = query; + } + + public static string GetQuery(this IQueryNode node) + { + return node.Data.TryGetValue(QueryKey, out object query) ? query as string : null; + } + + public static bool TryGetQuery(this IQueryNode node, out string query) + { + query = null; + return node.Data.TryGetValue(QueryKey, out object value) && (query = value as string) != null; + } + + public static void RemoveQuery(this IQueryNode node) + { + node.Data.Remove(QueryKey); + } } diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs index 085c58ad..3787fe3e 100644 --- a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs +++ b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs @@ -15,5 +15,6 @@ public class FieldInfo public bool IsNumber { get; set; } public bool IsDate { get; set; } public bool IsBoolean { get; set; } + public IDictionary Data { get; set; } = new Dictionary(); public List Children { get; set; } } diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs index 27e62519..58ee0474 100644 --- a/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs @@ -16,7 +16,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); // Employee - modelBuilder.Entity().HasIndex(e => new { e.FullName, e.Title, e.Age }); + modelBuilder.Entity().HasIndex(e => new { e.FullName, e.Title }); // Company modelBuilder.Entity().HasIndex(e => new { e.Name, e.Description }); @@ -40,7 +40,6 @@ public class Employee { public int Id { get; set; } public string FullName { get; set; } public string Title { get; set; } - public int Age { get; set; } public int CompanyId { get; set; } public Company Company { get; set; } public List DataValues { get; set; } diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs index 3f07b819..74741d93 100644 --- a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; -using Foundatio.Parsers.SqlQueries.Extensions; using Foundatio.Parsers.SqlQueries.Visitors; using Foundatio.Xunit; using Microsoft.EntityFrameworkCore; @@ -87,13 +86,10 @@ public async Task CanGenerateSql() { }); await context.SaveChangesAsync(); - var parser = new SqlQueryParser(); - parser.Configuration.UseFieldMap(new Dictionary {{ "age", "DataValues.Any(DataDefinitionId = 1 AND NumberValue" }}); - string sqlExpected = context.Employees.Where(e => e.Company.Name == "acme" && e.DataValues.Any(dv => dv.DataDefinitionId == 1 && dv.NumberValue == 30)).ToQueryString(); string sqlActual = context.Employees.Where("""company.name = "acme" AND DataValues.Any(DataDefinitionId = 1 AND NumberValue = 30) """).ToQueryString(); Assert.Equal(sqlExpected, sqlActual); - sqlActual = context.Employees.LuceneWhere("company.name:acme (age:1 OR age:>30)").ToQueryString(); + sqlActual = context.Employees.LuceneWhere("company.name:acme age:30").ToQueryString(); Assert.Equal(sqlExpected, sqlActual); var employees = await context.Employees.Where(e => e.Title == "software developer" && e.DataValues.Any(dv => dv.DataDefinitionId == 1 && dv.NumberValue == 30)) @@ -129,7 +125,7 @@ private async Task ParseAndValidateQuery(string query, string expected, bool isV string nodes = await DebugQueryVisitor.RunAsync(result); _logger.LogInformation(nodes); - string generatedQuery = await GenerateSqlVisitor.RunAsync(result); + string generatedQuery = await GenerateSqlVisitor.RunAsync(result, new SqlQueryVisitorContext()); Assert.Equal(expected, generatedQuery); } }