Skip to content

Commit e500cda

Browse files
authored
SQLite: implement MAX/MIN/ORDER BY for decimal (#35606)
1 parent 2b564f7 commit e500cda

7 files changed

+207
-61
lines changed

Diff for: src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs

+29-21
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,28 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
133133
LambdaExpression keySelector,
134134
bool ascending)
135135
{
136-
var translation = base.TranslateOrderBy(source, keySelector, ascending);
136+
var translation = TranslateLambdaExpression(source, keySelector);
137137
if (translation == null)
138138
{
139139
return null;
140140
}
141141

142-
var orderingExpression = ((SelectExpression)translation.QueryExpression).Orderings.Last();
143-
var orderingExpressionType = GetProviderType(orderingExpression.Expression);
142+
var orderingExpressionType = GetProviderType(translation);
144143
if (orderingExpressionType == typeof(DateTimeOffset)
145-
|| orderingExpressionType == typeof(decimal)
146144
|| orderingExpressionType == typeof(TimeSpan)
147145
|| orderingExpressionType == typeof(ulong))
148146
{
149147
throw new NotSupportedException(
150148
SqliteStrings.OrderByNotSupported(orderingExpressionType.ShortDisplayName()));
151149
}
150+
else if (orderingExpressionType == typeof(decimal))
151+
{
152+
translation = new CollateExpression(translation, "EF_DECIMAL");
153+
}
152154

153-
return translation;
155+
((SelectExpression)source.QueryExpression).ApplyOrdering(new OrderingExpression(translation, ascending));
156+
157+
return source;
154158
}
155159

156160
/// <summary>
@@ -164,24 +168,28 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
164168
LambdaExpression keySelector,
165169
bool ascending)
166170
{
167-
var translation = base.TranslateThenBy(source, keySelector, ascending);
171+
var translation = TranslateLambdaExpression(source, keySelector);
168172
if (translation == null)
169173
{
170174
return null;
171175
}
172176

173-
var orderingExpression = ((SelectExpression)translation.QueryExpression).Orderings.Last();
174-
var orderingExpressionType = GetProviderType(orderingExpression.Expression);
177+
var orderingExpressionType = GetProviderType(translation);
175178
if (orderingExpressionType == typeof(DateTimeOffset)
176-
|| orderingExpressionType == typeof(decimal)
177179
|| orderingExpressionType == typeof(TimeSpan)
178180
|| orderingExpressionType == typeof(ulong))
179181
{
180182
throw new NotSupportedException(
181183
SqliteStrings.OrderByNotSupported(orderingExpressionType.ShortDisplayName()));
182184
}
185+
else if (orderingExpressionType == typeof(decimal))
186+
{
187+
translation = new CollateExpression(translation, "EF_DECIMAL");
188+
}
189+
190+
((SelectExpression)source.QueryExpression).AppendOrdering(new OrderingExpression(translation, ascending));
183191

184-
return translation;
192+
return source;
185193
}
186194

187195
/// <summary>
@@ -467,9 +475,9 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
467475
Tables:
468476
[
469477
TableValuedFunctionExpression
470-
{
471-
Name: "json_each", Schema: null, IsBuiltIn: true, Arguments: [var jsonArrayColumn]
472-
} jsonEachExpression
478+
{
479+
Name: "json_each", Schema: null, IsBuiltIn: true, Arguments: [var jsonArrayColumn]
480+
} jsonEachExpression
473481
],
474482
Predicate: null,
475483
GroupBy: [],
@@ -529,16 +537,16 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
529537
protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
530538
{
531539
return selectExpression is
532-
{
533-
Tables: [var mainTable, ..],
534-
Orderings:
540+
{
541+
Tables: [var mainTable, ..],
542+
Orderings:
535543
[
536-
{
537-
Expression: ColumnExpression { Name: JsonEachKeyColumnName } orderingColumn,
538-
IsAscending: true
539-
}
544+
{
545+
Expression: ColumnExpression { Name: JsonEachKeyColumnName } orderingColumn,
546+
IsAscending: true
547+
}
540548
]
541-
}
549+
}
542550
&& orderingColumn.TableAlias == mainTable.Alias
543551
&& IsJsonEachKeyColumn(selectExpression, orderingColumn);
544552

Diff for: src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteQueryableAggregateMethodTranslator.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,23 @@ public SqliteQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExpress
7070
&& source.Selector is SqlExpression maxSqlExpression:
7171
var maxArgumentType = GetProviderType(maxSqlExpression);
7272
if (maxArgumentType == typeof(DateTimeOffset)
73-
|| maxArgumentType == typeof(decimal)
7473
|| maxArgumentType == typeof(TimeSpan)
7574
|| maxArgumentType == typeof(ulong))
7675
{
7776
throw new NotSupportedException(
7877
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Max), maxArgumentType.ShortDisplayName()));
7978
}
79+
else if (maxArgumentType == typeof(decimal))
80+
{
81+
maxSqlExpression = CombineTerms(source, maxSqlExpression);
82+
return _sqlExpressionFactory.Function(
83+
"ef_max",
84+
[maxSqlExpression],
85+
nullable: true,
86+
argumentsPropagateNullability: [false],
87+
maxSqlExpression.Type,
88+
maxSqlExpression.TypeMapping);
89+
}
8090

8191
break;
8292

@@ -86,13 +96,23 @@ public SqliteQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExpress
8696
&& source.Selector is SqlExpression minSqlExpression:
8797
var minArgumentType = GetProviderType(minSqlExpression);
8898
if (minArgumentType == typeof(DateTimeOffset)
89-
|| minArgumentType == typeof(decimal)
9099
|| minArgumentType == typeof(TimeSpan)
91100
|| minArgumentType == typeof(ulong))
92101
{
93102
throw new NotSupportedException(
94103
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Min), minArgumentType.ShortDisplayName()));
95104
}
105+
else if (minArgumentType == typeof(decimal))
106+
{
107+
minSqlExpression = CombineTerms(source, minSqlExpression);
108+
return _sqlExpressionFactory.Function(
109+
"ef_min",
110+
[minSqlExpression],
111+
nullable: true,
112+
argumentsPropagateNullability: [false],
113+
minSqlExpression.Type,
114+
minSqlExpression.TypeMapping);
115+
}
96116

97117
break;
98118

Diff for: src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs

+20
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,22 @@ private void InitializeDbConnection(DbConnection connection)
154154
: acc.sum / acc.count,
155155
isDeterministic: true);
156156

157+
sqliteConnection.CreateAggregate(
158+
"ef_max",
159+
seed: null,
160+
(decimal? max, decimal? value) => max is null
161+
? value
162+
: value is null ? max : decimal.Max(max.Value, value.Value),
163+
isDeterministic: true);
164+
165+
sqliteConnection.CreateAggregate(
166+
"ef_min",
167+
seed: null,
168+
(decimal? min, decimal? value) => min is null
169+
? value
170+
: value is null ? min : decimal.Min(min.Value, value.Value),
171+
isDeterministic: true);
172+
157173
sqliteConnection.CreateAggregate(
158174
"ef_sum",
159175
seed: null,
@@ -163,6 +179,10 @@ private void InitializeDbConnection(DbConnection connection)
163179
? value
164180
: sum.Value + value.Value,
165181
isDeterministic: true);
182+
183+
sqliteConnection.CreateCollation(
184+
"EF_DECIMAL",
185+
(x, y) => decimal.Compare(decimal.Parse(x), decimal.Parse(y)));
166186
}
167187
else
168188
{

Diff for: test/EFCore.Sqlite.FunctionalTests/BuiltInDataTypesSqliteTest.cs

+115-20
Original file line numberDiff line numberDiff line change
@@ -862,10 +862,7 @@ public virtual void Cant_query_Min_of_converted_types()
862862
.Where(e => e.PartitionId == 200)
863863
.GroupBy(_ => true);
864864

865-
Assert.Equal(
866-
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Min), typeof(decimal).ShortDisplayName()),
867-
Assert.Throws<NotSupportedException>(
868-
() => query.Select(g => g.Min(e => e.TestNullableDecimal)).ToList()).Message);
865+
Assert.Equal(2.000000000000001m, query.Select(g => g.Min(e => e.TestNullableDecimal)).Single());
869866

870867
Assert.Equal(
871868
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Min), typeof(DateTimeOffset).ShortDisplayName()),
@@ -915,10 +912,7 @@ public virtual void Cant_query_Max_of_converted_types()
915912
.Where(e => e.PartitionId == 201)
916913
.GroupBy(_ => true);
917914

918-
Assert.Equal(
919-
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Max), typeof(decimal).ShortDisplayName()),
920-
Assert.Throws<NotSupportedException>(
921-
() => query.Select(g => g.Max(e => e.TestNullableDecimal)).ToList()).Message);
915+
Assert.Equal(10.000000000000001m, query.Select(g => g.Max(e => e.TestNullableDecimal)).Single());
922916

923917
Assert.Equal(
924918
SqliteStrings.AggregateOperationNotSupported(nameof(Queryable.Max), typeof(DateTimeOffset).ShortDisplayName()),
@@ -1406,12 +1400,6 @@ public virtual void Cant_query_OrderBy_of_converted_types()
14061400
.Where(e => e.PartitionId == 205);
14071401

14081402
var ex = Assert.Throws<NotSupportedException>(
1409-
() => query
1410-
.OrderBy(e => e.TestNullableDecimal)
1411-
.First());
1412-
Assert.Equal(SqliteStrings.OrderByNotSupported("decimal"), ex.Message);
1413-
1414-
ex = Assert.Throws<NotSupportedException>(
14151403
() => query
14161404
.OrderBy(e => e.TestNullableDateTimeOffset)
14171405
.First());
@@ -1463,12 +1451,6 @@ public virtual void Cant_query_ThenBy_of_converted_types()
14631451
.OrderBy(e => e.PartitionId);
14641452

14651453
var ex = Assert.Throws<NotSupportedException>(
1466-
() => query
1467-
.ThenBy(e => e.TestNullableDecimal)
1468-
.First());
1469-
Assert.Equal(SqliteStrings.OrderByNotSupported("decimal"), ex.Message);
1470-
1471-
ex = Assert.Throws<NotSupportedException>(
14721454
() => query
14731455
.ThenBy(e => e.TestNullableDateTimeOffset)
14741456
.First());
@@ -1487,6 +1469,119 @@ public virtual void Cant_query_ThenBy_of_converted_types()
14871469
Assert.Equal(SqliteStrings.OrderByNotSupported("ulong"), ex.Message);
14881470
}
14891471

1472+
1473+
[ConditionalFact]
1474+
public virtual void Can_query_OrderBy_of_converted_types()
1475+
{
1476+
using var context = CreateContext();
1477+
var min = new BuiltInNullableDataTypes
1478+
{
1479+
Id = 221,
1480+
PartitionId = 207,
1481+
TestNullableDecimal = 2.000000000000001m,
1482+
TestNullableDateTimeOffset = new DateTimeOffset(2018, 1, 1, 12, 0, 0, TimeSpan.Zero),
1483+
TestNullableTimeSpan = TimeSpan.FromDays(2),
1484+
TestNullableUnsignedInt64 = 0
1485+
};
1486+
context.Add(min);
1487+
1488+
var max = new BuiltInNullableDataTypes
1489+
{
1490+
Id = 222,
1491+
PartitionId = 207,
1492+
TestNullableDecimal = 10.000000000000001m,
1493+
TestNullableDateTimeOffset = new DateTimeOffset(2018, 1, 1, 11, 0, 0, TimeSpan.FromHours(-2)),
1494+
TestNullableTimeSpan = TimeSpan.FromDays(10),
1495+
TestNullableUnsignedInt64 = long.MaxValue + 1ul
1496+
};
1497+
context.Add(max);
1498+
1499+
context.SaveChanges();
1500+
1501+
Fixture.TestSqlLoggerFactory.Clear();
1502+
1503+
var query = context.Set<BuiltInNullableDataTypes>()
1504+
.Where(e => e.PartitionId == 207);
1505+
1506+
var results = query
1507+
.OrderBy(e => e.TestNullableDecimal)
1508+
.Select(e => e.Id)
1509+
.First();
1510+
1511+
AssertSql(
1512+
"""
1513+
SELECT "b"."Id"
1514+
FROM "BuiltInNullableDataTypes" AS "b"
1515+
WHERE "b"."PartitionId" = 207
1516+
ORDER BY "b"."TestNullableDecimal" COLLATE EF_DECIMAL
1517+
LIMIT 1
1518+
""");
1519+
1520+
var expectedResults = query.AsEnumerable()
1521+
.OrderBy(e => e.TestNullableDecimal)
1522+
.Select(e => e.Id)
1523+
.First();
1524+
1525+
Assert.Equal(expectedResults, results);
1526+
}
1527+
1528+
[ConditionalFact]
1529+
public virtual void Can_query_ThenBy_of_converted_types()
1530+
{
1531+
using var context = CreateContext();
1532+
var min = new BuiltInNullableDataTypes
1533+
{
1534+
Id = 223,
1535+
PartitionId = 208,
1536+
TestNullableDecimal = 2.000000000000001m,
1537+
TestNullableDateTimeOffset = new DateTimeOffset(2018, 1, 1, 12, 0, 0, TimeSpan.Zero),
1538+
TestNullableTimeSpan = TimeSpan.FromDays(2),
1539+
TestNullableUnsignedInt64 = 0
1540+
};
1541+
context.Add(min);
1542+
1543+
var max = new BuiltInNullableDataTypes
1544+
{
1545+
Id = 224,
1546+
PartitionId = 208,
1547+
TestNullableDecimal = 10.000000000000001m,
1548+
TestNullableDateTimeOffset = new DateTimeOffset(2018, 1, 1, 11, 0, 0, TimeSpan.FromHours(-2)),
1549+
TestNullableTimeSpan = TimeSpan.FromDays(10),
1550+
TestNullableUnsignedInt64 = long.MaxValue + 1ul
1551+
};
1552+
context.Add(max);
1553+
1554+
context.SaveChanges();
1555+
1556+
Fixture.TestSqlLoggerFactory.Clear();
1557+
1558+
var query = context.Set<BuiltInNullableDataTypes>()
1559+
.Where(e => e.PartitionId == 208);
1560+
1561+
var results = query
1562+
.OrderBy(e => e.PartitionId)
1563+
.ThenBy(e => e.TestNullableDecimal)
1564+
.Select(e => e.Id)
1565+
.First();
1566+
1567+
AssertSql(
1568+
"""
1569+
SELECT "b"."Id"
1570+
FROM "BuiltInNullableDataTypes" AS "b"
1571+
WHERE "b"."PartitionId" = 208
1572+
ORDER BY "b"."PartitionId", "b"."TestNullableDecimal" COLLATE EF_DECIMAL
1573+
LIMIT 1
1574+
""");
1575+
1576+
var expectedResults = query.AsEnumerable()
1577+
.OrderBy(e => e.PartitionId)
1578+
.ThenBy(e => e.TestNullableDecimal)
1579+
.Select(e => e.Id)
1580+
.First();
1581+
1582+
Assert.Equal(expectedResults, results);
1583+
}
1584+
14901585
[ConditionalFact]
14911586
public virtual void Can_query_using_char_ToLower()
14921587
{

Diff for: test/EFCore.Sqlite.FunctionalTests/Query/Ef6GroupBySqliteTest.cs

+20-8
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,28 @@ GROUP BY "p"."Category"
2929
}
3030

3131
public override async Task Max_Grouped_from_LINQ_101(bool async)
32-
=> Assert.Equal(
33-
SqliteStrings.AggregateOperationNotSupported("Max", "decimal"),
34-
(await Assert.ThrowsAsync<NotSupportedException>(
35-
() => base.Max_Grouped_from_LINQ_101(async))).Message);
32+
{
33+
await base.Max_Grouped_from_LINQ_101(async);
34+
35+
AssertSql(
36+
"""
37+
SELECT "p"."Category", ef_max("p"."UnitPrice") AS "MostExpensivePrice"
38+
FROM "ProductForLinq" AS "p"
39+
GROUP BY "p"."Category"
40+
""");
41+
}
3642

3743
public override async Task Min_Grouped_from_LINQ_101(bool async)
38-
=> Assert.Equal(
39-
SqliteStrings.AggregateOperationNotSupported("Min", "decimal"),
40-
(await Assert.ThrowsAsync<NotSupportedException>(
41-
() => base.Min_Grouped_from_LINQ_101(async))).Message);
44+
{
45+
await base.Min_Grouped_from_LINQ_101(async);
46+
47+
AssertSql(
48+
"""
49+
SELECT "p"."Category", ef_min("p"."UnitPrice") AS "CheapestPrice"
50+
FROM "ProductForLinq" AS "p"
51+
GROUP BY "p"."Category"
52+
""");
53+
}
4254

4355
public override async Task Whats_new_2021_sample_3(bool async)
4456
=> await base.Whats_new_2021_sample_3(async);

0 commit comments

Comments
 (0)