Skip to content

Commit a01c6e1

Browse files
Upgrade to EF 10 (#165)
* Upgrade to EF 10 * Adding support for full text search * Revert * Potential fix for pull request finding 'Useless assignment to local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Ability to control which fields are full text indexed * Wait for SQL * Cleanup * Fix bugs with full-text --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent b55adfc commit a01c6e1

File tree

13 files changed

+414
-58
lines changed

13 files changed

+414
-58
lines changed

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM mcr.microsoft.com/mssql/server:2022-latest
2+
3+
ARG SSID_PID=Developer
4+
5+
ENV ACCEPT_EULA=Y
6+
ENV SSID_PID=${SSID_PID}
7+
ENV DEBIAN_FRONTEND=noninteractive
8+
ENV DEBCONF_NONINTERACTIVE_SEEN=true
9+
10+
USER root
11+
12+
RUN apt-get update && \
13+
apt-get upgrade -y && \
14+
apt-get install -yq gnupg gnupg2 gnupg1 curl apt-transport-https && \
15+
curl https://packages.microsoft.com/keys/microsoft.asc -o /var/opt/mssql/ms-key.cer && \
16+
gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg /var/opt/mssql/ms-key.cer && \
17+
curl https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list -o /etc/apt/sources.list.d/mssql-server-2022.list && \
18+
apt-get update && \
19+
apt-get install -y mssql-server-fts && \
20+
apt-get clean && \
21+
rm -rf /var/lib/apt/lists
22+
23+
ENTRYPOINT [ "/opt/mssql/bin/sqlservr" ]

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ services:
3030
test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:5601/api/status
3131

3232
sqlserver:
33-
image: mcr.microsoft.com/mssql/server:2025-latest
33+
build:
34+
context: .
35+
dockerfile: Dockerfile
3436
ports:
3537
- "1433:1433" # login with sa:P@ssword1
3638
environment:

src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
142142
{
143143
FieldInfo = fieldInfo,
144144
Term = node.Term,
145-
Operator = SqlSearchOperator.StartsWith
145+
Operator = context.DefaultSearchOperator
146146
};
147147
fieldTerms[fieldInfo] = searchTerm;
148148
}
@@ -164,10 +164,12 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
164164
var searchTerm = kvp.Value;
165165
var tokens = kvp.Value.Tokens ?? [kvp.Value.Term];
166166
var (fieldPrefix, fieldSuffix) = kvp.Key.GetFieldPrefixAndSuffix();
167+
var (scopePrefix, argumentPrefix) = SplitFieldPrefix(kvp.Key, fieldPrefix);
167168

168169
if (searchTerm.Operator == SqlSearchOperator.Equals)
169170
{
170-
builder.Append(fieldPrefix);
171+
builder.Append(scopePrefix);
172+
builder.Append(argumentPrefix);
171173
builder.Append(kvp.Key.Name);
172174
builder.Append(" in (");
173175
for (int i = 0; i < tokens.Count; i++)
@@ -185,11 +187,27 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
185187
tokens.ForEach((token, i) =>
186188
{
187189
builder.Append(i.IsFirst ? "(" : " OR ");
188-
builder.Append(fieldPrefix);
189-
builder.Append(kvp.Key.Name);
190-
builder.Append(".Contains(");
191-
AppendField(builder, kvp.Key, token, context);
192-
builder.Append(")");
190+
191+
builder.Append(scopePrefix);
192+
193+
if (context.FullTextFields.Contains(kvp.Key.FullName, StringComparer.OrdinalIgnoreCase))
194+
{
195+
builder.Append("FTS.Contains(");
196+
builder.Append(argumentPrefix);
197+
builder.Append(kvp.Key.Name);
198+
builder.Append(", ");
199+
AppendField(builder, kvp.Key, token, context);
200+
builder.Append(")");
201+
}
202+
else
203+
{
204+
builder.Append(argumentPrefix);
205+
builder.Append(kvp.Key.Name);
206+
builder.Append(".Contains(");
207+
AppendField(builder, kvp.Key, token, context);
208+
builder.Append(")");
209+
}
210+
193211
builder.Append(fieldSuffix);
194212
if (i.IsLast)
195213
builder.Append(")");
@@ -200,11 +218,26 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
200218
tokens.ForEach((token, i) =>
201219
{
202220
builder.Append(i.IsFirst ? "(" : " OR ");
203-
builder.Append(fieldPrefix);
204-
builder.Append(kvp.Key.Name);
205-
builder.Append(".StartsWith(");
206-
AppendField(builder, kvp.Key, token, context);
207-
builder.Append(")");
221+
builder.Append(scopePrefix);
222+
223+
if (context.FullTextFields.Contains(kvp.Key.FullName, StringComparer.OrdinalIgnoreCase))
224+
{
225+
builder.Append("FTS.Contains(");
226+
builder.Append(argumentPrefix);
227+
builder.Append(kvp.Key.Name);
228+
builder.Append(", ");
229+
AppendField(builder, kvp.Key, "\\\"" + token + "*\\\"", context);
230+
builder.Append(")");
231+
}
232+
else
233+
{
234+
builder.Append(argumentPrefix);
235+
builder.Append(kvp.Key.Name);
236+
builder.Append(".StartsWith(");
237+
AppendField(builder, kvp.Key, token, context);
238+
builder.Append(")");
239+
}
240+
208241
builder.Append(fieldSuffix);
209242
if (i.IsLast)
210243
builder.Append(")");
@@ -220,6 +253,7 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
220253

221254
var field = GetFieldInfo(context.Fields, node.Field);
222255
var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();
256+
var (scopePrefix, argumentPrefix) = SplitFieldPrefix(field, fieldPrefix);
223257
var searchOperator = SqlSearchOperator.Equals;
224258
if (node.Term.StartsWith("*") && node.Term.EndsWith("*"))
225259
searchOperator = SqlSearchOperator.Contains;
@@ -231,28 +265,59 @@ public static string ToDynamicLinqString(this TermNode node, ISqlQueryVisitorCon
231265

232266
if (searchOperator == SqlSearchOperator.Equals)
233267
{
234-
builder.Append(fieldPrefix);
268+
builder.Append(scopePrefix);
269+
builder.Append(argumentPrefix);
235270
builder.Append(field.Name);
236271
builder.Append(" = ");
237272
AppendField(builder, field, node.Term, context);
238273
builder.Append(fieldSuffix);
239274
}
240275
else if (searchOperator == SqlSearchOperator.Contains)
241276
{
242-
builder.Append(fieldPrefix);
243-
builder.Append(field.Name);
244-
builder.Append(".Contains(");
245-
AppendField(builder, field, node.Term, context);
246-
builder.Append(")");
277+
builder.Append(scopePrefix);
278+
279+
if (context.FullTextFields.Contains(field.FullName, StringComparer.OrdinalIgnoreCase))
280+
{
281+
builder.Append("FTS.Contains(");
282+
builder.Append(argumentPrefix);
283+
builder.Append(field.Name);
284+
builder.Append(", ");
285+
AppendField(builder, field, node.Term, context);
286+
builder.Append(")");
287+
}
288+
else
289+
{
290+
builder.Append(argumentPrefix);
291+
builder.Append(field.Name);
292+
builder.Append(".Contains(");
293+
AppendField(builder, field, node.Term, context);
294+
builder.Append(")");
295+
}
296+
247297
builder.Append(fieldSuffix);
248298
}
249299
else
250300
{
251-
builder.Append(fieldPrefix);
252-
builder.Append(field.Name);
253-
builder.Append(".StartsWith(");
254-
AppendField(builder, field, node.Term, context);
255-
builder.Append(")");
301+
builder.Append(scopePrefix);
302+
303+
if (context.FullTextFields.Contains(field.FullName, StringComparer.OrdinalIgnoreCase))
304+
{
305+
builder.Append("FTS.Contains(");
306+
builder.Append(argumentPrefix);
307+
builder.Append(field.Name);
308+
builder.Append(", ");
309+
AppendField(builder, field, "\\\"" + node.Term + "*\\\"", context);
310+
builder.Append(")");
311+
}
312+
else
313+
{
314+
builder.Append(argumentPrefix);
315+
builder.Append(field.Name);
316+
builder.Append(".Contains(");
317+
AppendField(builder, field, node.Term, context);
318+
builder.Append(")");
319+
}
320+
256321
builder.Append(fieldSuffix);
257322
}
258323

@@ -277,6 +342,7 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
277342
context.AddValidationError("Field must be a number, money or date for term range queries.");
278343

279344
var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();
345+
var (scopePrefix, argumentPrefix) = SplitFieldPrefix(field, fieldPrefix);
280346

281347
var builder = new StringBuilder();
282348

@@ -288,7 +354,8 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
288354

289355
if (node.Min != null)
290356
{
291-
builder.Append(fieldPrefix);
357+
builder.Append(scopePrefix);
358+
builder.Append(argumentPrefix);
292359
builder.Append(field.Name);
293360
builder.Append(node.MinInclusive == true ? " >= " : " > ");
294361
AppendField(builder, field, node.Min, context);
@@ -300,7 +367,8 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
300367

301368
if (node.Max != null)
302369
{
303-
builder.Append(fieldPrefix);
370+
builder.Append(scopePrefix);
371+
builder.Append(argumentPrefix);
304372
builder.Append(field.Name);
305373
builder.Append(node.MaxInclusive == true ? " <= " : " < ");
306374
AppendField(builder, field, node.Max, context);
@@ -335,6 +403,18 @@ public static EntityFieldInfo GetFieldInfo(List<EntityFieldInfo> fields, string
335403
new EntityFieldInfo { Name = field, FullName = field };
336404
}
337405

406+
private static (string scopePrefix, string argumentPrefix) SplitFieldPrefix(EntityFieldInfo field, string fieldPrefix)
407+
{
408+
string navigationPrefix = field.GetNavigationPrefix();
409+
if (String.IsNullOrEmpty(navigationPrefix))
410+
return (fieldPrefix, String.Empty);
411+
412+
if (!String.IsNullOrEmpty(fieldPrefix) && fieldPrefix.EndsWith(navigationPrefix, StringComparison.Ordinal))
413+
fieldPrefix = fieldPrefix.Substring(0, fieldPrefix.Length - navigationPrefix.Length);
414+
415+
return (fieldPrefix, navigationPrefix);
416+
}
417+
338418
private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term, ISqlQueryVisitorContext context)
339419
{
340420
if (field == null)

src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="..\..\build\common.props" />
33
<PropertyGroup>
4-
<TargetFrameworks>net8.0;</TargetFrameworks>
4+
<TargetFrameworks>net10.0;</TargetFrameworks>
55
</PropertyGroup>
66
<ItemGroup>
7-
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.20" />
7+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
88
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="4.0.1" />
9-
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.9" />
10-
</ItemGroup>
11-
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
12-
<PackageReference Include="System.Text.Json" Version="8.0.5" />
9+
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
1310
</ItemGroup>
1411
<ItemGroup>
1512
<ProjectReference Include="..\..\src\Foundatio.Parsers.LuceneQueries\Foundatio.Parsers.LuceneQueries.csproj" />

src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
using System.Collections.Generic;
44
using System.ComponentModel.DataAnnotations;
55
using System.Linq;
6+
using System.Linq.Dynamic.Core;
7+
using System.Linq.Dynamic.Core.CustomTypeProviders;
68
using System.Threading.Tasks;
79
using Foundatio.Parsers.LuceneQueries;
810
using Foundatio.Parsers.LuceneQueries.Extensions;
911
using Foundatio.Parsers.LuceneQueries.Nodes;
1012
using Foundatio.Parsers.LuceneQueries.Visitors;
1113
using Foundatio.Parsers.SqlQueries.Extensions;
1214
using Foundatio.Parsers.SqlQueries.Visitors;
15+
using Microsoft.EntityFrameworkCore;
1316
using Microsoft.EntityFrameworkCore.Metadata;
1417
using Pegasus.Common;
1518

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

2730
public SqlQueryParserConfiguration Configuration { get; }
31+
public ParsingConfig ParsingConfig { get; } = new()
32+
{
33+
CustomTypeProvider = new DynamicLinqTypeProvider()
34+
};
2835

2936
public override async Task<IQueryNode> ParseAsync(string query, IQueryVisitorContext context = null)
3037
{
@@ -217,6 +224,18 @@ private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
217224
sqlContext.SearchTokenizer = Configuration.SearchTokenizer;
218225
sqlContext.DateTimeParser = Configuration.DateTimeParser;
219226
sqlContext.DateOnlyParser = Configuration.DateOnlyParser;
227+
sqlContext.DefaultSearchOperator = Configuration.DefaultFieldsSearchOperator;
228+
sqlContext.FullTextFields = Configuration.FullTextFields;
220229
}
221230
}
222231
}
232+
233+
public static class FTS
234+
{
235+
public static bool Contains(string propertyValue, string searchTerm)
236+
{
237+
return EF.Functions.Contains(propertyValue, searchTerm);
238+
}
239+
}
240+
241+
public class DynamicLinqTypeProvider() : DefaultDynamicLinqCustomTypeProvider(ParsingConfig.Default, [ typeof(EF), typeof(FTS) ]);

src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public SqlQueryParserConfiguration()
2323

2424
public ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
2525
public string[] DefaultFields { get; private set; }
26-
26+
public SqlSearchOperator DefaultFieldsSearchOperator { get; private set; } = SqlSearchOperator.StartsWith;
27+
public string[] FullTextFields { get; private set; }
2728
public int MaxFieldDepth { get; private set; } = 10;
2829
public QueryFieldResolver FieldResolver { get; private set; }
2930
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
@@ -46,9 +47,10 @@ public SqlQueryParserConfiguration SetLoggerFactory(ILoggerFactory loggerFactory
4647
return this;
4748
}
4849

49-
public SqlQueryParserConfiguration SetDefaultFields(string[] fields)
50+
public SqlQueryParserConfiguration SetDefaultFields(string[] fields, SqlSearchOperator op = SqlSearchOperator.StartsWith)
5051
{
5152
DefaultFields = fields;
53+
DefaultFieldsSearchOperator = op;
5254
return this;
5355
}
5456

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

63+
public SqlQueryParserConfiguration SetFullTextFields(string[] fields)
64+
{
65+
FullTextFields = fields;
66+
return this;
67+
}
68+
6169
public SqlQueryParserConfiguration SetDateTimeParser(Func<string, string> dateTimeParser)
6270
{
6371
DateTimeParser = dateTimeParser;

src/Foundatio.Parsers.SqlQueries/Visitors/ISqlQueryVisitorContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Foundatio.Parsers.SqlQueries.Visitors;
77
public interface ISqlQueryVisitorContext : IQueryVisitorContext
88
{
99
List<EntityFieldInfo> Fields { get; set; }
10+
SqlSearchOperator DefaultSearchOperator { get; set; }
11+
string[] FullTextFields { get; set; }
1012
Action<SearchTerm> SearchTokenizer { get; set; }
1113
Func<string, string> DateTimeParser { get; set; }
1214
Func<string, string> DateOnlyParser { get; set; }

0 commit comments

Comments
 (0)