Skip to content

Commit

Permalink
Add support for DateOnly
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith committed Jan 13, 2025
1 parent 5e6b322 commit 89d0706
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 1 deletion.
31 changes: 30 additions & 1 deletion src/Foundatio.Parsers.SqlQueries/Extensions/SqlNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 + "\"");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/Foundatio.Parsers.SqlQueries/SqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ private void AddEntityFields(List<EntityFieldInfo> 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
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
25 changes: 25 additions & 0 deletions tests/Foundatio.Parsers.LuceneQueries.Tests/QueryParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Foundatio.Xunit;
using Microsoft.Extensions.Logging;
using Pegasus.Common;
using Pegasus.Common.Tracing;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions tests/Foundatio.Parsers.SqlQueries.Tests/SampleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class Employee
public int Salary { get; set; }
public List<Company> Companies { get; set; }
public List<DataValue> DataValues { get; set; }
public TimeOnly HappyHour { get; set; }
public DateOnly Birthday { get; set; }

public DateTime Created { get; set; } = DateTime.Now;
}
Expand All @@ -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; }
Expand All @@ -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,
Expand Down Expand Up @@ -172,6 +176,7 @@ public enum DataType
Number,
Boolean,
Date,
DateOnly,
Money,
Percent
}
36 changes: 36 additions & 0 deletions tests/Foundatio.Parsers.SqlQueries.Tests/SqlQueryParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SqlQueryParser>();

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:<now-90d", context);
sqlActual = db.Employees.Where(sql).ToQueryString();
Assert.Equal(sqlExpected, sqlActual);
}

[Fact]
public async Task CanUseTimeOnlyFilter()
{
var sp = GetServiceProvider();
await using var db = await GetSampleContextWithDataAsync(sp);
var parser = sp.GetRequiredService<SqlQueryParser>();

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()
{
Expand Down Expand Up @@ -388,6 +422,7 @@ public async Task<SampleContext> 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]
});
Expand All @@ -398,6 +433,7 @@ public async Task<SampleContext> 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]
});
Expand Down

0 comments on commit 89d0706

Please sign in to comment.