diff --git a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
index 791f876..5c7a618 100644
--- a/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
+++ b/src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
@@ -267,7 +267,7 @@ public static string ToDynamicLinqString(this TermRangeNode node, ISqlQueryVisit
return query;
var field = GetFieldInfo(context.Fields, node.Field);
- if (!field.IsNumber && !field.IsDate && !field.IsMoney)
+ if (!field.IsNumber && !field.IsDateOnly && !field.IsDate && !field.IsMoney)
context.AddValidationError("Field must be a number, money or date for term range queries.");
var (fieldPrefix, fieldSuffix) = field.GetFieldPrefixAndSuffix();
@@ -371,6 +371,35 @@ private static void AppendField(StringBuilder builder, EntityFieldInfo field, st
builder.Append("DateTime.Parse(\"" + term + "\")");
}
}
+ else if (field is { IsDateOnly: true })
+ {
+ term = term.Trim();
+ if (term.StartsWith("now", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append("DateOnly.FromDateTime(DateTime.UtcNow)");
+
+ if (term.Length == 3)
+ return;
+
+ builder.Append(".");
+
+ string method = term[^1..] switch
+ {
+ "y" => "AddYears",
+ "M" => "AddMonths",
+ "d" => "AddDays",
+ _ => throw new NotSupportedException("Invalid date operation.")
+ };
+
+ bool subtract = term.Substring(3, 1) == "-";
+
+ builder.Append(method).Append("(").Append(subtract ? "-" : "").Append(term.Substring(4, term.Length - 5)).Append(")");
+ }
+ else
+ {
+ builder.Append("DateOnly.Parse(\"" + term + "\")");
+ }
+ }
else
builder.Append("\"" + term + "\"");
}
diff --git a/src/Foundatio.Parsers.SqlQueries/Extensions/TypeExtensions.cs b/src/Foundatio.Parsers.SqlQueries/Extensions/TypeExtensions.cs
index 1592177..e4f2c56 100644
--- a/src/Foundatio.Parsers.SqlQueries/Extensions/TypeExtensions.cs
+++ b/src/Foundatio.Parsers.SqlQueries/Extensions/TypeExtensions.cs
@@ -35,6 +35,7 @@ public static Type UnwrapNullable(this Type type)
public static bool IsString(this Type type) => type == typeof(string);
public static bool IsDateTime(this Type typeToCheck) => typeToCheck == typeof(DateTime) || typeToCheck == typeof(DateTime?);
+ public static bool IsDateOnly(this Type typeToCheck) => typeToCheck == typeof(DateOnly) || typeToCheck == typeof(DateOnly?);
public static bool IsBoolean(this Type typeToCheck) => typeToCheck == typeof(bool) || typeToCheck == typeof(bool?);
public static bool IsNumeric(this Type type) => type.IsFloatingPoint() || type.IsIntegerBased();
public static bool IsIntegerBased(this Type type) => _integerTypes.Contains(type);
diff --git a/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj b/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj
index d0d46a9..72f5c00 100644
--- a/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj
+++ b/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs b/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
index 93a76ef..c729fa5 100644
--- a/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
+++ b/src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
@@ -127,6 +127,7 @@ private void AddEntityFields(List fields, EntityFieldInfo paren
FullName = propertyPath,
IsNumber = property.ClrType.UnwrapNullable().IsNumeric(),
IsDate = property.ClrType.UnwrapNullable().IsDateTime(),
+ IsDateOnly = property.ClrType.UnwrapNullable().IsDateOnly(),
IsBoolean = property.ClrType.UnwrapNullable().IsBoolean(),
Parent = parent
});
diff --git a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs
index d8c8f88..98bce30 100644
--- a/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs
+++ b/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs
@@ -24,6 +24,7 @@ public class EntityFieldInfo
public bool IsNumber { get; set; }
public bool IsMoney { get; set; }
public bool IsDate { get; set; }
+ public bool IsDateOnly { get; set; }
public bool IsBoolean { get; set; }
public bool IsCollection { get; set; }
public bool IsNavigation { get; set; }
diff --git a/tests/Foundatio.Parsers.LuceneQueries.Tests/QueryParserTests.cs b/tests/Foundatio.Parsers.LuceneQueries.Tests/QueryParserTests.cs
index 8b31100..3112b18 100644
--- a/tests/Foundatio.Parsers.LuceneQueries.Tests/QueryParserTests.cs
+++ b/tests/Foundatio.Parsers.LuceneQueries.Tests/QueryParserTests.cs
@@ -8,6 +8,7 @@
using Foundatio.Xunit;
using Microsoft.Extensions.Logging;
using Pegasus.Common;
+using Pegasus.Common.Tracing;
using Xunit;
using Xunit.Abstractions;
@@ -137,6 +138,30 @@ public async Task CanParseRegex()
}
}
+ [Fact]
+ public void DataBackslashShouldBeValidBeginningOfString()
+ {
+ var sut = new LuceneQueryParser();
+ var result = sut.Parse("\"\\something\"");
+ _logger.LogInformation(DebugQueryVisitor.Run(result));
+ }
+
+ [Fact]
+ public void CanHandleDateRange()
+ {
+#if FALSE
+ var tracer = new LoggingTracer(_logger, reportPerformance: true);
+#else
+ var tracer = NullTracer.Instance;
+#endif
+ var sut = new LuceneQueryParser {
+ Tracer = tracer
+ };
+ string query = "mydate:[now/d TO now/d+30d/d]";
+ var result = sut.Parse(query);
+ _logger.LogInformation(DebugQueryVisitor.Run(result));
+ }
+
[Fact]
public void CanHandleEmpty()
{
diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/DynamicFieldVisitor.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/DynamicFieldVisitor.cs
index 7e62071..5c4b099 100644
--- a/tests/Foundatio.Parsers.SqlQueries.Tests/DynamicFieldVisitor.cs
+++ b/tests/Foundatio.Parsers.SqlQueries.Tests/DynamicFieldVisitor.cs
@@ -40,6 +40,9 @@ public override IQueryNode Visit(TermNode node, IQueryVisitorContext context)
case { IsDate: true }:
customFieldBuilder.Append("DateValue");
break;
+ case { IsDateOnly: true }:
+ customFieldBuilder.Append("DateOnlyValue");
+ break;
default:
customFieldBuilder.Append("StringValue");
break;
diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj b/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj
index 1d6e5f3..66a006d 100644
--- a/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj
+++ b/tests/Foundatio.Parsers.SqlQueries.Tests/Foundatio.Parsers.SqlQueries.Tests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
index 2d106b0..e99d5cc 100644
--- a/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
+++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
@@ -47,6 +47,8 @@ public class Employee
public int Salary { get; set; }
public List Companies { get; set; }
public List DataValues { get; set; }
+ public TimeOnly HappyHour { get; set; }
+ public DateOnly Birthday { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
}
@@ -70,6 +72,7 @@ public class DataValue
// store the values separately as sparse columns for querying purposes
public string StringValue { get; set; }
public DateTime? DateValue { get; set; }
+ public DateOnly? DateOnlyValue { get; set; }
public decimal? MoneyValue { get; set; }
public bool? BooleanValue { get; set; }
public decimal? NumberValue { get; set; }
@@ -87,6 +90,7 @@ public object GetValue(DataType? dataType = null)
{
DataType.String => StringValue,
DataType.Date => DateValue,
+ DataType.DateOnly => DateOnlyValue,
DataType.Number => NumberValue,
DataType.Boolean => BooleanValue,
DataType.Money => MoneyValue,
@@ -172,6 +176,7 @@ public enum DataType
Number,
Boolean,
Date,
+ DateOnly,
Money,
Percent
}
diff --git a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs
index 9e5e441..2ab01a3 100644
--- a/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs
+++ b/tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs
@@ -164,6 +164,40 @@ public async Task CanUseDateFilter()
Assert.Equal(sqlExpected, sqlActual);
}
+ [Fact]
+ public async Task CanUseDateOnlyFilter()
+ {
+ var sp = GetServiceProvider();
+ await using var db = await GetSampleContextWithDataAsync(sp);
+ var parser = sp.GetRequiredService();
+
+ var context = parser.GetContext(db.Employees.EntityType);
+
+ string sqlExpected = db.Employees.Where(e => e.Birthday < DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-90)).ToQueryString();
+ string sqlActual = db.Employees.Where("""birthday < DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-90)""").ToQueryString();
+ Assert.Equal(sqlExpected, sqlActual);
+ string sql = await parser.ToDynamicLinqAsync("birthday:();
+
+ var context = parser.GetContext(db.Employees.EntityType);
+
+ string sqlExpected = db.Employees.Where(e => e.HappyHour < TimeOnly.Parse("6:00")).ToQueryString();
+ string sqlActual = db.Employees.Where("""happyhour < TimeOnly.Parse("6:00")""").ToQueryString();
+ Assert.Equal(sqlExpected, sqlActual);
+ string sql = await parser.ToDynamicLinqAsync("""happyhour:<"6:00" """, context);
+ sqlActual = db.Employees.Where(sql).ToQueryString();
+ Assert.Equal(sqlExpected, sqlActual);
+ }
+
[Fact]
public async Task CanUseExistsFilter()
{
@@ -388,6 +422,7 @@ public async Task GetSampleContextWithDataAsync(IServiceProvider
PhoneNumber = "(214) 222-2222",
NationalPhoneNumber = phoneNumberUtil.Parse("(214) 222-2222", "US").NationalNumber.ToString(),
Salary = 80_000,
+ Birthday = new DateOnly(1980, 1, 1),
DataValues = [new() { Definition = company.DataDefinitions[0], NumberValue = 30 }],
Companies = [company]
});
@@ -398,6 +433,7 @@ public async Task GetSampleContextWithDataAsync(IServiceProvider
PhoneNumber = "+52 55 1234 5678", // Mexico
NationalPhoneNumber = phoneNumberUtil.Parse("+52 55 1234 5678", "US").NationalNumber.ToString(),
Salary = 90_000,
+ Birthday = new DateOnly(1972, 11, 6),
DataValues = [new() { Definition = company.DataDefinitions[0], NumberValue = 23 }],
Companies = [company]
});