Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/mssql/server:2022-latest

ARG SSID_PID=Developer

ENV ACCEPT_EULA=Y
ENV SSID_PID=${SSID_PID}
ENV DEBIAN_FRONTEND=noninteractive
ENV DEBCONF_NONINTERACTIVE_SEEN=true

USER root

RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -yq gnupg gnupg2 gnupg1 curl apt-transport-https && \
curl https://packages.microsoft.com/keys/microsoft.asc -o /var/opt/mssql/ms-key.cer && \
gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg /var/opt/mssql/ms-key.cer && \
curl https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list -o /etc/apt/sources.list.d/mssql-server-2022.list && \
apt-get update && \
apt-get install -y mssql-server-fts && \
apt-get clean && \
rm -rf /var/lib/apt/lists

ENTRYPOINT [ "/opt/mssql/bin/sqlservr" ]
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ services:
test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:5601/api/status

sqlserver:
image: mcr.microsoft.com/mssql/server:2025-latest
build:
context: .
dockerfile: Dockerfile
ports:
- "1433:1433" # login with sa:P@ssword1
environment:
Expand Down
86 changes: 69 additions & 17 deletions src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
{
FieldInfo = fieldInfo,
Term = node.Term,
Operator = SqlSearchOperator.StartsWith
Operator = context.DefaultSearchOperator
};
fieldTerms[fieldInfo] = searchTerm;
}
Expand Down Expand Up @@ -186,10 +186,23 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
{
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(fieldPrefix);
builder.Append(kvp.Key.Name);
builder.Append(".Contains(");
AppendField(builder, kvp.Key, token, context);
builder.Append(")");

if (context.FullTextFields.Contains(kvp.Key.Name, StringComparer.OrdinalIgnoreCase))
{
builder.Append("FTS.Contains(");
builder.Append(kvp.Key.Name);
builder.Append(", ");
AppendField(builder, kvp.Key, token, context);
builder.Append(")");
}
else
{
builder.Append(kvp.Key.Name);
builder.Append(".Contains(");
AppendField(builder, kvp.Key, token, context);
builder.Append(")");
}

builder.Append(fieldSuffix);
if (i.IsLast)
builder.Append(")");
Expand All @@ -201,10 +214,23 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
{
builder.Append(i.IsFirst ? "(" : " OR ");
builder.Append(fieldPrefix);
builder.Append(kvp.Key.Name);
builder.Append(".StartsWith(");
AppendField(builder, kvp.Key, token, context);
builder.Append(")");

if (context.FullTextFields.Contains(kvp.Key.Name, StringComparer.OrdinalIgnoreCase))
{
builder.Append("FTS.Contains(");
builder.Append(kvp.Key.Name);
builder.Append(", ");
AppendField(builder, kvp.Key, "\\\"" + token + "*\\\"", context);
builder.Append(")");
}
else
{
builder.Append(kvp.Key.Name);
builder.Append(".StartsWith(");
AppendField(builder, kvp.Key, token, context);
builder.Append(")");
}

builder.Append(fieldSuffix);
if (i.IsLast)
builder.Append(")");
Expand Down Expand Up @@ -240,19 +266,45 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
else if (searchOperator == SqlSearchOperator.Contains)
{
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(".Contains(");
AppendField(builder, field, node.Term, context);
builder.Append(")");

if (context.FullTextFields.Contains(field.Name, StringComparer.OrdinalIgnoreCase))
{
builder.Append("FTS.Contains(");
builder.Append(field.Name);
builder.Append(", ");
AppendField(builder, field, node.Term, context);
builder.Append(")");
}
else
{
builder.Append(field.Name);
builder.Append(".Contains(");
AppendField(builder, field, node.Term, context);
builder.Append(")");
}

builder.Append(fieldSuffix);
}
else
{
builder.Append(fieldPrefix);
builder.Append(field.Name);
builder.Append(".StartsWith(");
AppendField(builder, field, node.Term, context);
builder.Append(")");

if (context.FullTextFields.Contains(field.Name, StringComparer.OrdinalIgnoreCase))
{
builder.Append("FTS.Contains(");
builder.Append(field.Name);
builder.Append(", ");
AppendField(builder, field, "\\\"" + node.Term + "*\\\"", context);
builder.Append(")");
}
else
{
builder.Append(field.Name);
builder.Append(".Contains(");
AppendField(builder, field, node.Term, context);
builder.Append(")");
}

builder.Append(fieldSuffix);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>net8.0;</TargetFrameworks>
<TargetFrameworks>net10.0;</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="4.0.1" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.9" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Foundatio.Parsers.LuceneQueries\Foundatio.Parsers.LuceneQueries.csproj" />
Expand Down
19 changes: 19 additions & 0 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.CustomTypeProviders;
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.Extensions;
using Foundatio.Parsers.SqlQueries.Visitors;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Pegasus.Common;

Expand All @@ -25,6 +28,10 @@ public SqlQueryParser(Action<SqlQueryParserConfiguration> configure = null)
}

public SqlQueryParserConfiguration Configuration { get; }
public ParsingConfig ParsingConfig { get; } = new()
{
CustomTypeProvider = new DynamicLinqTypeProvider()
};

public override async Task<IQueryNode> ParseAsync(string query, IQueryVisitorContext context = null)
{
Expand Down Expand Up @@ -217,6 +224,18 @@ private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
sqlContext.SearchTokenizer = Configuration.SearchTokenizer;
sqlContext.DateTimeParser = Configuration.DateTimeParser;
sqlContext.DateOnlyParser = Configuration.DateOnlyParser;
sqlContext.DefaultSearchOperator = Configuration.DefaultFieldsSearchOperator;
sqlContext.FullTextFields = Configuration.FullTextFields;
}
}
}

public static class FTS
{
public static bool Contains(string propertyValue, string searchTerm)
{
return EF.Functions.Contains(propertyValue, searchTerm);
}
}

public class DynamicLinqTypeProvider() : DefaultDynamicLinqCustomTypeProvider(ParsingConfig.Default, [ typeof(EF), typeof(FTS) ]);
12 changes: 10 additions & 2 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public SqlQueryParserConfiguration()

public ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
public string[] DefaultFields { get; private set; }

public SqlSearchOperator DefaultFieldsSearchOperator { get; private set; } = SqlSearchOperator.StartsWith;
public string[] FullTextFields { get; private set; }
public int MaxFieldDepth { get; private set; } = 10;
public QueryFieldResolver FieldResolver { get; private set; }
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
Expand All @@ -46,9 +47,10 @@ public SqlQueryParserConfiguration SetLoggerFactory(ILoggerFactory loggerFactory
return this;
}

public SqlQueryParserConfiguration SetDefaultFields(string[] fields)
public SqlQueryParserConfiguration SetDefaultFields(string[] fields, SqlSearchOperator op = SqlSearchOperator.StartsWith)
{
DefaultFields = fields;
DefaultFieldsSearchOperator = op;
return this;
}

Expand All @@ -58,6 +60,12 @@ public SqlQueryParserConfiguration SetSearchTokenizer(Action<SearchTerm> tokeniz
return this;
}

public SqlQueryParserConfiguration SetFullTextFields(string[] fields)
{
FullTextFields = fields;
return this;
}

public SqlQueryParserConfiguration SetDateTimeParser(Func<string, string> dateTimeParser)
{
DateTimeParser = dateTimeParser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Foundatio.Parsers.SqlQueries.Visitors;
public interface ISqlQueryVisitorContext : IQueryVisitorContext
{
List<EntityFieldInfo> Fields { get; set; }
SqlSearchOperator DefaultSearchOperator { get; set; }
string[] FullTextFields { get; set; }
Action<SearchTerm> SearchTokenizer { get; set; }
Func<string, string> DateTimeParser { get; set; }
Func<string, string> DateOnlyParser { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace Foundatio.Parsers.SqlQueries.Visitors;
public class SqlQueryVisitorContext : QueryVisitorContext, ISqlQueryVisitorContext
{
public List<EntityFieldInfo> Fields { get; set; }
public SqlSearchOperator DefaultSearchOperator { get; set; } = SqlSearchOperator.StartsWith;
public string[] FullTextFields { get; set; } = [];
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
public Func<string, string> DateTimeParser { get; set; }
public Func<string, string> DateOnlyParser { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="All" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;</TargetFrameworks>
<!--<DefineConstants>ENABLE_TRACING</DefineConstants>-->
<NoWarn>$(NoWarn);CS8002;</NoWarn>
</PropertyGroup>
Expand All @@ -9,7 +10,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="libphonenumber-csharp" Version="9.0.18" />
</ItemGroup>

Expand Down
11 changes: 11 additions & 0 deletions tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Foundatio.Parsers.SqlQueries.Tests;

Expand Down Expand Up @@ -34,6 +36,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<DataValue>().Property(e => e.BooleanValue).IsSparse();
modelBuilder.Entity<DataValue>().Property(e => e.NumberValue).HasColumnType("decimal").HasPrecision(15, 3).IsSparse();
modelBuilder.Entity<DataValue>().HasIndex(e => new { e.StringValue, e.DateValue, e.MoneyValue, e.BooleanValue, e.NumberValue });

modelBuilder.HasDbFunction(typeof(FTS).GetMethod(nameof(FTS.Contains))!)
.HasTranslation(args => new SqlFunctionExpression(
"CONTAINS",
args,
true,
args.Select(a => false).ToList(),
typeof(bool),
null));
}
}

Expand Down
Loading
Loading