diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs index d338a331..f065d01a 100644 --- a/src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs +++ b/src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using System.Threading.Tasks; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; @@ -11,29 +12,29 @@ public class GenerateSqlVisitor : QueryNodeVisitorWithResultBase public override Task VisitAsync(GroupNode node, IQueryVisitorContext context) { - _builder.Append(node.ToString(context != null ? context.DefaultOperator : GroupOperator.Default)); + _builder.Append(node.ToSqlString(context?.DefaultOperator ?? GroupOperator.Default)); return Task.CompletedTask; } public override void Visit(TermNode node, IQueryVisitorContext context) { - _builder.Append(node); + _builder.Append(node.ToSqlString()); } public override void Visit(TermRangeNode node, IQueryVisitorContext context) { - _builder.Append(node); + _builder.Append(node.ToSqlString()); } public override void Visit(ExistsNode node, IQueryVisitorContext context) { - _builder.Append(node); + _builder.Append(node.ToSqlString()); } public override void Visit(MissingNode node, IQueryVisitorContext context) { - _builder.Append(node); + _builder.Append(node.ToSqlString()); } public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlNodeExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlNodeExtensions.cs new file mode 100644 index 00000000..043a27d6 --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlNodeExtensions.cs @@ -0,0 +1,161 @@ +using System; +using System.Text; +using Foundatio.Parsers.LuceneQueries.Nodes; + +namespace Foundatio.Parsers.SqlQueries.Visitors; + +public static class SqlNodeExtensions +{ + public static string ToSqlString(this GroupNode node, GroupOperator defaultOperator = GroupOperator.Default) + { + if (node.Left == null && node.Right == null) + return String.Empty; + + var builder = new StringBuilder(); + var op = node.Operator != GroupOperator.Default ? node.Operator : defaultOperator; + + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append("NOT "); + + builder.Append(node.Prefix); + + if (!String.IsNullOrEmpty(node.Field)) + builder.Append(node.Field).Append(':'); + + if (node.HasParens) + builder.Append("("); + + if (node.Left != null) + builder.Append(node.Left is GroupNode groupNode ? groupNode.ToSqlString(defaultOperator) : node.Left.ToSqlString()); + + if (node.Left != null && node.Right != null) + { + if (op == GroupOperator.And) + builder.Append(" AND "); + else if (op == GroupOperator.Or) + builder.Append(" OR "); + else if (node.Right != null) + builder.Append(" "); + } + + if (node.Right != null) + builder.Append(node.Right is GroupNode groupNode ? groupNode.ToSqlString(defaultOperator) : node.Right.ToSqlString()); + + if (node.HasParens) + builder.Append(")"); + + if (node.Proximity != null) + builder.Append("~" + node.Proximity); + + if (node.Boost != null) + builder.Append("^" + node.Boost); + + return builder.ToString(); + } + + public static string ToSqlString(this ExistsNode node) + { + var builder = new StringBuilder(); + + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append("NOT "); + + builder.Append(node.Prefix); + builder.Append("_exists_"); + builder.Append(":"); + builder.Append(node.Field); + + return builder.ToString(); + } + + public static string ToSqlString(this MissingNode node) + { + var builder = new StringBuilder(); + + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append("NOT "); + + builder.Append(node.Prefix); + builder.Append("_missing_"); + builder.Append(":"); + builder.Append(node.Field); + + return builder.ToString(); + } + + public static string ToSqlString(this TermNode node) + { + var builder = new StringBuilder(); + + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append("NOT "); + + if (!String.IsNullOrEmpty(node.Field)) + { + builder.Append(node.Field); + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append(" != "); + else + builder.Append(" = "); + } + + builder.Append("\"" + node.Term + "\""); + + return builder.ToString(); + } + + public static string ToSqlString(this TermRangeNode node) + { + var builder = new StringBuilder(); + + if (node.IsNegated.HasValue && node.IsNegated.Value) + builder.Append("NOT "); + + builder.Append(node.Prefix); + + if (!String.IsNullOrEmpty(node.Field)) + { + builder.Append(node.Field); + builder.Append(":"); + } + + if (!String.IsNullOrEmpty(node.Operator)) + builder.Append(node.Operator); + + if (node.MinInclusive.HasValue && String.IsNullOrEmpty(node.Operator)) + builder.Append(node.MinInclusive.Value ? "[" : "{"); + + if (node.Min != null) + builder.Append(node.Min); + + if (node.Delimiter != null) + builder.Append(node.Delimiter); + + if (node.Max != null) + builder.Append(node.Max); + + if (node.MaxInclusive.HasValue && String.IsNullOrEmpty(node.Operator)) + builder.Append(node.MaxInclusive.Value ? "]" : "}"); + + if (node.Boost != null) + builder.Append("^" + node.Boost); + + if (node.Proximity != null) + builder.Append("~" + node.Proximity); + + return builder.ToString(); + } + + public static string ToSqlString(this IQueryNode node, GroupOperator defaultOperator = GroupOperator.Default) + { + return node switch + { + GroupNode groupNode => groupNode.ToSqlString(defaultOperator), + ExistsNode existsNode => existsNode.ToSqlString(), + MissingNode missingNode => missingNode.ToSqlString(), + TermNode termNode => termNode.ToSqlString(), + TermRangeNode termRangeNode => termRangeNode.ToSqlString(), + _ => throw new NotSupportedException($"Node type {node.GetType().Name} is not supported.") + }; + } +} diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs index 2dc2f573..367e150c 100644 --- a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs @@ -48,12 +48,12 @@ public async Task CanGenerateSql() { await context.SaveChangesAsync(); var parser = new SqlQueryParser(); - parser.Configuration.UseFieldMap(new Dictionary {{ "age", "DataValues.Any(DataDefinitionId = 1 AND NumberValue = " }}); + parser.Configuration.UseFieldMap(new Dictionary {{ "age", "DataValues.Any(DataDefinitionId = 1 AND NumberValue" }}); // translate AST to dynamic linq // lookup custom fields and convert to sql // know what data type each column is in order to know if it support range operators var node = await parser.ParseAsync("""company.name:acme age:30"""); - + string sql = await GenerateSqlVisitor.RunAsync(node); 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();