diff --git a/Foundatio.Parsers.sln b/Foundatio.Parsers.sln index 3f223df9..71a4681a 100644 --- a/Foundatio.Parsers.sln +++ b/Foundatio.Parsers.sln @@ -22,6 +22,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.Parsers.ElasticQueries.Tests", "tests\Foundatio.Parsers.ElasticQueries.Tests\Foundatio.Parsers.ElasticQueries.Tests.csproj", "{C353E601-874C-4855-9B01-2980BC624BC4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundatio.Parsers.SqlQueries", "src\Foundatio.Parsers.SqlQueries\Foundatio.Parsers.SqlQueries.csproj", "{53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundatio.Parsers.SqlQueries.Tests", "tests\Foundatio.Parsers.SqlQueries.Tests\Foundatio.Parsers.SqlQueries.Tests.csproj", "{B30934E7-C69F-465B-BACF-61AAEC7DA775}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,6 +48,14 @@ Global {C353E601-874C-4855-9B01-2980BC624BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {C353E601-874C-4855-9B01-2980BC624BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {C353E601-874C-4855-9B01-2980BC624BC4}.Release|Any CPU.Build.0 = Release|Any CPU + {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Release|Any CPU.Build.0 = Release|Any CPU + {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/common.props b/build/common.props index 2f61f25c..2f0d9505 100644 --- a/build/common.props +++ b/build/common.props @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0; Foundatio.Parsers A lucene style query parser that is extensible and allows additional syntax features. https://github.com/FoundatioFx/Foundatio.Parsers diff --git a/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj b/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj new file mode 100644 index 00000000..5f3398d4 --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs b/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs new file mode 100644 index 00000000..9004c04d --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Foundatio.Parsers.LuceneQueries; +using Foundatio.Parsers.LuceneQueries.Extensions; +using Foundatio.Parsers.LuceneQueries.Nodes; +using Foundatio.Parsers.LuceneQueries.Visitors; +using Foundatio.Parsers.SqlQueries.Visitors; +using Pegasus.Common; + +namespace Foundatio.Parsers.SqlQueries; + +public class SqlQueryParser : LuceneQueryParser { + public SqlQueryParser(Action configure = null) { + var config = new SqlQueryParserConfiguration(); + configure?.Invoke(config); + Configuration = config; + } + + public SqlQueryParserConfiguration Configuration { get; } + + public override async Task ParseAsync(string query, IQueryVisitorContext context = null) { + query ??= String.Empty; + + if (context == null) + context = new SqlQueryVisitorContext(); + + //SetupQueryVisitorContextDefaults(context); + try { + var result = await base.ParseAsync(query, context).ConfigureAwait(false); + switch (context.QueryType) { + case QueryTypes.Aggregation: + result = await Configuration.AggregationVisitor.AcceptAsync(result, context).ConfigureAwait(false); + break; + case QueryTypes.Query: + result = await Configuration.QueryVisitor.AcceptAsync(result, context).ConfigureAwait(false); + break; + case QueryTypes.Sort: + result = await Configuration.SortVisitor.AcceptAsync(result, context).ConfigureAwait(false); + break; + } + + return result; + } catch (FormatException ex) { + var cursor = ex.Data["cursor"] as Cursor; + context.GetValidationResult().QueryType = context.QueryType; + context.AddValidationError(ex.Message, cursor.Column); + + return null; + } + } +} \ No newline at end of file diff --git a/src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs b/src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs new file mode 100644 index 00000000..0f663521 --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Foundatio.Parsers.LuceneQueries; +using Foundatio.Parsers.LuceneQueries.Visitors; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Foundatio.Parsers.SqlQueries; + +public class SqlQueryParserConfiguration { + private ILogger _logger = NullLogger.Instance; + + public SqlQueryParserConfiguration() { + //AddQueryVisitor(new CombineQueriesVisitor(), 10000); + AddSortVisitor(new TermToFieldVisitor(), 0); + AddAggregationVisitor(new AssignOperationTypeVisitor(), 0); + //AddAggregationVisitor(new CombineAggregationsVisitor(), 10000); + AddVisitor(new FieldResolverQueryVisitor((field, context) => FieldResolver != null ? FieldResolver(field, context) : Task.FromResult(null)), 10); + AddVisitor(new ValidationVisitor(), 30); + } + + public ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance; + public string[] DefaultFields { get; private set; } + public QueryFieldResolver FieldResolver { get; private set; } + public IncludeResolver IncludeResolver { get; private set; } + //public ElasticMappingResolver MappingResolver { get; private set; } + public QueryValidationOptions ValidationOptions { get; private set; } + public ChainedQueryVisitor SortVisitor { get; } = new ChainedQueryVisitor(); + public ChainedQueryVisitor QueryVisitor { get; } = new ChainedQueryVisitor(); + public ChainedQueryVisitor AggregationVisitor { get; } = new ChainedQueryVisitor(); + + public SqlQueryParserConfiguration SetLoggerFactory(ILoggerFactory loggerFactory) { + LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = loggerFactory.CreateLogger(); + + return this; + } + + public SqlQueryParserConfiguration SetDefaultFields(string[] fields) { + DefaultFields = fields; + return this; + } + + public SqlQueryParserConfiguration UseFieldResolver(QueryFieldResolver resolver, int priority = 10) { + FieldResolver = resolver; + ReplaceVisitor(new FieldResolverQueryVisitor(resolver), priority); + + return this; + } + + public SqlQueryParserConfiguration UseFieldMap(IDictionary fields, int priority = 10) { + if (fields != null) + return UseFieldResolver(fields.ToHierarchicalFieldResolver(), priority); + + return UseFieldResolver(null); + } + + public SqlQueryParserConfiguration UseIncludes(IncludeResolver includeResolver, ShouldSkipIncludeFunc shouldSkipInclude = null, int priority = 0) { + IncludeResolver = includeResolver; + + return AddVisitor(new IncludeVisitor(shouldSkipInclude), priority); + } + + public SqlQueryParserConfiguration UseIncludes(Func resolveInclude, ShouldSkipIncludeFunc shouldSkipInclude = null, int priority = 0) { + return UseIncludes(name => Task.FromResult(resolveInclude(name)), shouldSkipInclude, priority); + } + + public SqlQueryParserConfiguration UseIncludes(IDictionary includes, ShouldSkipIncludeFunc shouldSkipInclude = null, int priority = 0) { + return UseIncludes(name => includes.ContainsKey(name) ? includes[name] : null, shouldSkipInclude, priority); + } + + public SqlQueryParserConfiguration SetValidationOptions(QueryValidationOptions options) { + ValidationOptions = options; + return this; + } + + #region Combined Visitor Management + + public SqlQueryParserConfiguration AddVisitor(IChainableQueryVisitor visitor, int priority = 0) { + QueryVisitor.AddVisitor(visitor, priority); + AggregationVisitor.AddVisitor(visitor, priority); + SortVisitor.AddVisitor(visitor, priority); + + return this; + } + + public SqlQueryParserConfiguration RemoveVisitor() where T : IChainableQueryVisitor { + QueryVisitor.RemoveVisitor(); + AggregationVisitor.RemoveVisitor(); + SortVisitor.RemoveVisitor(); + + return this; + } + + public SqlQueryParserConfiguration ReplaceVisitor(IChainableQueryVisitor visitor, int? newPriority = null) where T : IChainableQueryVisitor { + QueryVisitor.ReplaceVisitor(visitor, newPriority); + AggregationVisitor.ReplaceVisitor(visitor, newPriority); + SortVisitor.ReplaceVisitor(visitor, newPriority); + + return this; + } + + public SqlQueryParserConfiguration AddVisitorBefore(IChainableQueryVisitor visitor) { + QueryVisitor.AddVisitorBefore(visitor); + AggregationVisitor.AddVisitorBefore(visitor); + SortVisitor.AddVisitorBefore(visitor); + + return this; + } + + public SqlQueryParserConfiguration AddVisitorAfter(IChainableQueryVisitor visitor) { + QueryVisitor.AddVisitorAfter(visitor); + AggregationVisitor.AddVisitorAfter(visitor); + SortVisitor.AddVisitorAfter(visitor); + + return this; + } + + #endregion + + #region Query Visitor Management + + public SqlQueryParserConfiguration AddQueryVisitor(IChainableQueryVisitor visitor, int priority = 0) { + QueryVisitor.AddVisitor(visitor, priority); + + return this; + } + + public SqlQueryParserConfiguration RemoveQueryVisitor() where T : IChainableQueryVisitor { + QueryVisitor.RemoveVisitor(); + + return this; + } + + public SqlQueryParserConfiguration ReplaceQueryVisitor(IChainableQueryVisitor visitor, int? newPriority = null) where T : IChainableQueryVisitor { + QueryVisitor.ReplaceVisitor(visitor, newPriority); + + return this; + } + + public SqlQueryParserConfiguration AddQueryVisitorBefore(IChainableQueryVisitor visitor) { + QueryVisitor.AddVisitorBefore(visitor); + + return this; + } + + public SqlQueryParserConfiguration AddQueryVisitorAfter(IChainableQueryVisitor visitor) { + QueryVisitor.AddVisitorAfter(visitor); + + return this; + } + + #endregion + + #region Sort Visitor Management + + public SqlQueryParserConfiguration AddSortVisitor(IChainableQueryVisitor visitor, int priority = 0) { + SortVisitor.AddVisitor(visitor, priority); + + return this; + } + + public SqlQueryParserConfiguration RemoveSortVisitor() where T : IChainableQueryVisitor { + SortVisitor.RemoveVisitor(); + + return this; + } + + public SqlQueryParserConfiguration ReplaceSortVisitor(IChainableQueryVisitor visitor, int? newPriority = null) where T : IChainableQueryVisitor { + SortVisitor.ReplaceVisitor(visitor, newPriority); + + return this; + } + + public SqlQueryParserConfiguration AddSortVisitorBefore(IChainableQueryVisitor visitor) { + SortVisitor.AddVisitorBefore(visitor); + + return this; + } + + public SqlQueryParserConfiguration AddSortVisitorAfter(IChainableQueryVisitor visitor) { + SortVisitor.AddVisitorAfter(visitor); + + return this; + } + + #endregion + + #region Aggregation Visitor Management + + public SqlQueryParserConfiguration AddAggregationVisitor(IChainableQueryVisitor visitor, int priority = 0) { + AggregationVisitor.AddVisitor(visitor, priority); + + return this; + } + + public SqlQueryParserConfiguration RemoveAggregationVisitor() where T : IChainableQueryVisitor { + AggregationVisitor.RemoveVisitor(); + + return this; + } + + public SqlQueryParserConfiguration ReplaceAggregationVisitor(IChainableQueryVisitor visitor, int? newPriority = null) where T : IChainableQueryVisitor { + AggregationVisitor.ReplaceVisitor(visitor, newPriority); + + return this; + } + + public SqlQueryParserConfiguration AddAggregationVisitorBefore(IChainableQueryVisitor visitor) { + AggregationVisitor.AddVisitorBefore(visitor); + + return this; + } + + public SqlQueryParserConfiguration AddAggregationVisitorAfter(IChainableQueryVisitor visitor) { + AggregationVisitor.AddVisitorAfter(visitor); + + return this; + } + + #endregion +} diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/ISqlQueryVisitorContext.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/ISqlQueryVisitorContext.cs new file mode 100644 index 00000000..6dd1102b --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/Visitors/ISqlQueryVisitorContext.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Foundatio.Parsers.LuceneQueries.Visitors; + +namespace Foundatio.Parsers.SqlQueries.Visitors { + public interface ISqlQueryVisitorContext : IQueryVisitorContext { + Func> DefaultTimeZone { get; set; } + bool UseScoring { get; set; } + //ElasticMappingResolver MappingResolver { get; set; } + } +} diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs new file mode 100644 index 00000000..17ee2f10 --- /dev/null +++ b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Foundatio.Parsers.LuceneQueries.Visitors; + +namespace Foundatio.Parsers.SqlQueries.Visitors; + +public class SqlQueryVisitorContext : QueryVisitorContext, ISqlQueryVisitorContext { + public Func> DefaultTimeZone { get; set; } + public bool UseScoring { get; set; } + //public ElasticMappingResolver MappingResolver { get; set; } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index e000e4b2..698b1bae 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,14 +2,15 @@ net8.0 + net8.0; False $(NoWarn);CS1591;NU1701 - - - - + + + + diff --git a/tests/Foundatio.Parsers.LuceneQueries.Tests/GenerateQueryVisitorTests.cs b/tests/Foundatio.Parsers.LuceneQueries.Tests/GenerateQueryVisitorTests.cs index 5de81e31..11244b29 100644 --- a/tests/Foundatio.Parsers.LuceneQueries.Tests/GenerateQueryVisitorTests.cs +++ b/tests/Foundatio.Parsers.LuceneQueries.Tests/GenerateQueryVisitorTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj b/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj new file mode 100644 index 00000000..40df7cb8 --- /dev/null +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs new file mode 100644 index 00000000..78101ae2 --- /dev/null +++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Foundatio.Parsers.LuceneQueries.Nodes; +using Foundatio.Parsers.SqlQueries.Visitors; +using Foundatio.Xunit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Foundatio.Parsers.SqlQueries.Tests; + +public class SqlQueryParserTests : TestWithLoggingBase { + public SqlQueryParserTests(ITestOutputHelper output) : base(output) { + Log.MinimumLevel = LogLevel.Trace; + } + + [Theory] + [InlineData("value1 value2", GroupOperator.Default, "value1 value2")] + [InlineData("value1 value2", GroupOperator.And, "value1 AND value2")] + [InlineData("value1 value2", GroupOperator.Or, "value1 OR value2")] + [InlineData("value1 value2 value3", GroupOperator.Default, "value1 value2 value3")] + [InlineData("value1 value2 value3", GroupOperator.And, "value1 AND value2 AND value3")] + [InlineData("value1 value2 value3", GroupOperator.Or, "value1 OR value2 OR value3")] + [InlineData("value1 value2 value3 value4", GroupOperator.And, "value1 AND value2 AND value3 AND value4")] + [InlineData("(value1 value2) OR (value3 value4)", GroupOperator.And, "(value1 AND value2) OR (value3 AND value4)")] + public async Task DefaultOperatorApplied(string query, GroupOperator groupOperator, string expected) { + var contextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var context = new SampleContext(contextOptions); + var company = new Company { Name = "Acme" }; + context.Companies.Add(company); + context.Employees.Add(new Employee { FullName = "John Doe", Title = "Software Developer", Company = company }); + context.Employees.Add(new Employee { FullName = "Jane Doe", Title = "Software Developer", Company = company }); + await context.SaveChangesAsync(); + + var parser = new SqlQueryParser(config => config.SetDefaultFields(["FullName", "Title"])); + var result = await parser.ParseAsync(query, new SqlQueryVisitorContext { DefaultOperator = groupOperator }); + Assert.NotNull(result); + Assert.Equal(expected, result.ToString()); + } +} + +public class SampleContext : DbContext { + public SampleContext(DbContextOptions options) : base(options) { } + public DbSet Employees { get; set; } + public DbSet Companies { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + base.OnConfiguring(optionsBuilder); + } +} + +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 class Company { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List Employees { get; set; } +}