Skip to content

Commit

Permalink
Some progress on SQL translation
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith committed Mar 11, 2024
1 parent 59284db commit dbe9842
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 8 deletions.
13 changes: 7 additions & 6 deletions src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,29 +12,29 @@ public class GenerateSqlVisitor : QueryNodeVisitorWithResultBase<string>

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<string> AcceptAsync(IQueryNode node, IQueryVisitorContext context)
Expand Down
161 changes: 161 additions & 0 deletions src/Foundatio.Parsers.SqlQueries/Visitors/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
@@ -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.")
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ public async Task CanGenerateSql() {
await context.SaveChangesAsync();

var parser = new SqlQueryParser();
parser.Configuration.UseFieldMap(new Dictionary<string, string> {{ "age", "DataValues.Any(DataDefinitionId = 1 AND NumberValue = " }});
parser.Configuration.UseFieldMap(new Dictionary<string, string> {{ "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();
Expand Down

0 comments on commit dbe9842

Please sign in to comment.