Skip to content

Commit 8117723

Browse files
committed
Ability to provide custom datetime and dateonly parsing
1 parent 694974c commit 8117723

File tree

7 files changed

+101
-9
lines changed

7 files changed

+101
-9
lines changed

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
287287
builder.Append(fieldPrefix);
288288
builder.Append(field.Name);
289289
builder.Append(node.MinInclusive == true ? " >= " : " > ");
290-
AppendField(builder, field, node.Min);
290+
AppendField(builder, field, node.Min, context);
291291
builder.Append(fieldSuffix);
292292
}
293293

@@ -299,7 +299,7 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
299299
builder.Append(fieldPrefix);
300300
builder.Append(field.Name);
301301
builder.Append(node.MaxInclusive == true ? " <= " : " < ");
302-
AppendField(builder, field, node.Max);
302+
AppendField(builder, field, node.Max, context);
303303
builder.Append(fieldSuffix);
304304
}
305305

@@ -331,7 +331,7 @@ public static EntityFieldInfo GetFieldInfo(List<EntityFieldInfo> fields, string
331331
new EntityFieldInfo { Name = field, FullName = field };
332332
}
333333

334-
private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term)
334+
private static void AppendField(StringBuilder builder, EntityFieldInfo field, string term, ISqlQueryVisitorContext context)
335335
{
336336
if (field == null)
337337
return;
@@ -343,9 +343,10 @@ private static void AppendField(StringBuilder builder, EntityFieldInfo field, st
343343
else if (field is { IsDate: true })
344344
{
345345
term = term.Trim();
346+
346347
if (term.StartsWith("now", StringComparison.OrdinalIgnoreCase))
347348
{
348-
builder.Append("DateTime.UtcNow");
349+
builder.Append(context.DateTimeParser != null ? context.DateTimeParser("now") : "DateTime.UtcNow");
349350

350351
if (term.Length == 3)
351352
return;
@@ -370,15 +371,26 @@ private static void AppendField(StringBuilder builder, EntityFieldInfo field, st
370371
}
371372
else
372373
{
373-
builder.Append("DateTime.Parse(\"" + term + "\", null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)");
374+
if (context.DateTimeParser != null)
375+
{
376+
term = context.DateTimeParser(term);
377+
builder.Append(term);
378+
}
379+
else
380+
{
381+
builder.Append("DateTime.Parse(\"" + term + "\")");
382+
}
374383
}
375384
}
376385
else if (field is { IsDateOnly: true })
377386
{
378387
term = term.Trim();
388+
379389
if (term.StartsWith("now", StringComparison.OrdinalIgnoreCase))
380390
{
381-
builder.Append("DateOnly.FromDateTime(DateTime.UtcNow)");
391+
builder.Append(context.DateOnlyParser != null
392+
? context.DateOnlyParser("now")
393+
: "DateOnly.FromDateTime(DateTime.UtcNow)");
382394

383395
if (term.Length == 3)
384396
return;
@@ -399,7 +411,15 @@ private static void AppendField(StringBuilder builder, EntityFieldInfo field, st
399411
}
400412
else
401413
{
402-
builder.Append("DateOnly.Parse(\"" + term + "\")");
414+
if (context.DateOnlyParser != null)
415+
{
416+
term = context.DateOnlyParser(term);
417+
builder.Append(term);
418+
}
419+
else
420+
{
421+
builder.Append("DateOnly.Parse(\"" + term + "\")");
422+
}
403423
}
404424
}
405425
else

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.15" />
88
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
99
<PackageReference Include="System.Text.Json" Version="8.0.5" />
10-
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
10+
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.3" />
1111
</ItemGroup>
1212
<ItemGroup>
1313
<ProjectReference Include="..\..\src\Foundatio.Parsers.LuceneQueries\Foundatio.Parsers.LuceneQueries.csproj" />

src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
215215
if (context is ISqlQueryVisitorContext sqlContext)
216216
{
217217
sqlContext.SearchTokenizer = Configuration.SearchTokenizer;
218+
sqlContext.DateTimeParser = Configuration.DateTimeParser;
219+
sqlContext.DateOnlyParser = Configuration.DateOnlyParser;
218220
}
219221
}
220222
}

src/Foundatio.Parsers.SqlQueries/SqlQueryParserConfiguration.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public SqlQueryParserConfiguration()
2727
public int MaxFieldDepth { get; private set; } = 10;
2828
public QueryFieldResolver FieldResolver { get; private set; }
2929
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
30+
public Func<string, string> DateTimeParser { get; set; } = DefaultTimeParser;
31+
public Func<string, string> DateOnlyParser { get; set; } = DefaultOnlyParser;
3032
public EntityTypePropertyFilter EntityTypePropertyFilter { get; private set; } = static _ => true;
3133
public EntityTypeNavigationFilter EntityTypeNavigationFilter { get; private set; } = static _ => true;
3234
public EntityTypeSkipNavigationFilter EntityTypeSkipNavigationFilter { get; private set; } = static _ => true;
@@ -39,7 +41,7 @@ public SqlQueryParserConfiguration()
3941
public SqlQueryParserConfiguration SetLoggerFactory(ILoggerFactory loggerFactory)
4042
{
4143
LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
42-
_logger = loggerFactory.CreateLogger<SqlQueryParserConfiguration>();
44+
_logger = loggerFactory!.CreateLogger<SqlQueryParserConfiguration>();
4345

4446
return this;
4547
}
@@ -56,6 +58,18 @@ public SqlQueryParserConfiguration SetSearchTokenizer(Action<SearchTerm> tokeniz
5658
return this;
5759
}
5860

61+
public SqlQueryParserConfiguration SetDateTimeParser(Func<string, string> dateTimeParser)
62+
{
63+
DateTimeParser = dateTimeParser;
64+
return this;
65+
}
66+
67+
public SqlQueryParserConfiguration SetDateOnlyParser(Func<string, string> dateOnlyParser)
68+
{
69+
DateOnlyParser = dateOnlyParser;
70+
return this;
71+
}
72+
5973
public SqlQueryParserConfiguration SetFieldDepth(int maxFieldDepth)
6074
{
6175
MaxFieldDepth = maxFieldDepth;
@@ -119,6 +133,22 @@ public SqlQueryParserConfiguration SetValidationOptions(QueryValidationOptions o
119133
return this;
120134
}
121135

136+
public static string DefaultTimeParser(string dateTimeValue)
137+
{
138+
if (dateTimeValue.Equals("now", StringComparison.OrdinalIgnoreCase))
139+
return "DateTime.UtcNow";
140+
141+
return "DateTime.Parse(\"" + dateTimeValue + "\")";
142+
}
143+
144+
public static string DefaultOnlyParser(string dateOnlyValue)
145+
{
146+
if (dateOnlyValue.Equals("now", StringComparison.OrdinalIgnoreCase))
147+
return "DateOnly.FromDateTime(DateTime.UtcNow)";
148+
149+
return "DateOnly.Parse(\"" + dateOnlyValue + "\")";
150+
}
151+
122152
#region Combined Visitor Management
123153

124154
public SqlQueryParserConfiguration AddVisitor(IChainableQueryVisitor visitor, int priority = 0)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public interface ISqlQueryVisitorContext : IQueryVisitorContext
88
{
99
List<EntityFieldInfo> Fields { get; set; }
1010
Action<SearchTerm> SearchTokenizer { get; set; }
11+
Func<string, string> DateTimeParser { get; set; }
12+
Func<string, string> DateOnlyParser { get; set; }
1113
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class SqlQueryVisitorContext : QueryVisitorContext, ISqlQueryVisitorConte
1111
{
1212
public List<EntityFieldInfo> Fields { get; set; }
1313
public Action<SearchTerm> SearchTokenizer { get; set; } = static _ => { };
14+
public Func<string, string> DateTimeParser { get; set; }
15+
public Func<string, string> DateOnlyParser { get; set; }
1416
public IEntityType EntityType { get; set; }
1517
}
1618

tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,42 @@ public async Task CanUseDateFilter()
164164
Assert.Equal(sqlExpected, sqlActual);
165165
}
166166

167+
[Fact]
168+
public async Task CanUseDateParser()
169+
{
170+
var sp = GetServiceProvider();
171+
await using var db = await GetSampleContextWithDataAsync(sp);
172+
var parser = sp.GetRequiredService<SqlQueryParser>();
173+
var tz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
174+
var utcNow = DateTime.UtcNow;
175+
_logger.LogInformation("UtcNow: {UtcNow:O} {UtcTicks}", utcNow, utcNow.Ticks);
176+
var localNow = TimeZoneInfo.ConvertTimeFromUtc(utcNow, tz);
177+
_logger.LogInformation("LocalNow: {LocalNow:O}", localNow);
178+
179+
parser.Configuration.DateTimeParser = dateTimeValue =>
180+
{
181+
if (dateTimeValue.Equals("now", StringComparison.OrdinalIgnoreCase))
182+
return "DateTime.UtcNow";
183+
184+
var dateTime = DateTime.Parse(dateTimeValue);
185+
_logger.LogInformation("Parsed DateTime: {DateTime:O} {DateTimeKind}", dateTime, dateTime.Kind);
186+
187+
if (dateTime.Kind != DateTimeKind.Utc)
188+
dateTime = TimeZoneInfo.ConvertTimeToUtc(dateTime, tz);
189+
190+
_logger.LogInformation("Parsed UTC DateTime: {DateTime:O} {UtcTicks}", dateTime, dateTime.Ticks);
191+
return "DateTime(" + dateTime.Ticks + ", DateTimeKind.Utc)";
192+
};
193+
194+
var context = parser.GetContext(db.Employees.EntityType);
195+
196+
string sqlActual = db.Employees.Where($"created > DateTime({utcNow.Ticks}, DateTimeKind.Utc)").ToQueryString();
197+
Assert.Contains(utcNow.ToString("O"), sqlActual);
198+
string sql = await parser.ToDynamicLinqAsync($"created:>\"{localNow:O}\"", context);
199+
sqlActual = db.Employees.Where(sql).ToQueryString();
200+
Assert.Contains(utcNow.ToString("O"), sqlActual);
201+
}
202+
167203
[Fact]
168204
public async Task CanUseDateOnlyFilter()
169205
{

0 commit comments

Comments
 (0)