Skip to content

Commit 6c9cb94

Browse files
committed
Support primitive collections
Closes dotnet#29427 Closes dotnet#30426 Closes dotnet#13617
1 parent 55ec594 commit 6c9cb94

File tree

90 files changed

+5594
-598
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+5594
-598
lines changed

All.sln.DotSettings

+2
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,10 @@ The .NET Foundation licenses this file to you under the MIT license.
308308
<s:Boolean x:Key="/Default/UserDictionary/Words/=navigations/@EntryIndexedValue">True</s:Boolean>
309309
<s:Boolean x:Key="/Default/UserDictionary/Words/=niladic/@EntryIndexedValue">True</s:Boolean>
310310
<s:Boolean x:Key="/Default/UserDictionary/Words/=NOCASE/@EntryIndexedValue">True</s:Boolean>
311+
<s:Boolean x:Key="/Default/UserDictionary/Words/=OPENJSON/@EntryIndexedValue">True</s:Boolean>
311312
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralizer/@EntryIndexedValue">True</s:Boolean>
312313
<s:Boolean x:Key="/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue">True</s:Boolean>
314+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
313315
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
314316
<s:Boolean x:Key="/Default/UserDictionary/Words/=remapper/@EntryIndexedValue">True</s:Boolean>
315317
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@
346346
<data name="EitherOfTwoValuesMustBeNull" xml:space="preserve">
347347
<value>Either {param1} or {param2} must be null.</value>
348348
</data>
349+
<data name="EmptyCollectionNotSupportedAsConstantQueryRoot" xml:space="preserve">
350+
<value>Empty constant collections are not supported as constant query roots.</value>
351+
</data>
349352
<data name="EntityShortNameNotUnique" xml:space="preserve">
350353
<value>The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity&lt;TEntity&gt;().Metadata.SetDiscriminatorValue("NewShortName").</value>
351354
</data>

src/EFCore.Relational/Query/Internal/ContainsTranslator.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,10 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
5757
}
5858

5959
private static bool ValidateValues(SqlExpression values)
60-
=> values is SqlConstantExpression || values is SqlParameterExpression;
60+
=> values is SqlConstantExpression or SqlParameterExpression;
6161

6262
private static SqlExpression RemoveObjectConvert(SqlExpression expression)
63-
=> expression is SqlUnaryExpression sqlUnaryExpression
64-
&& sqlUnaryExpression.OperatorType == ExpressionType.Convert
63+
=> expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression
6564
&& sqlUnaryExpression.Type == typeof(object)
6665
? sqlUnaryExpression.Operand
6766
: expression;

src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ public SelectExpressionProjectionApplyingExpressionVisitor(QuerySplittingBehavio
3535
protected override Expression VisitExtension(Expression extensionExpression)
3636
=> extensionExpression switch
3737
{
38-
ShapedQueryExpression shapedQueryExpression
39-
when shapedQueryExpression.QueryExpression is SelectExpression selectExpression
38+
ShapedQueryExpression { QueryExpression: SelectExpression selectExpression } shapedQueryExpression
4039
=> shapedQueryExpression.UpdateShaperExpression(
4140
selectExpression.ApplyProjection(
4241
shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior)),
42+
4343
NonQueryExpression nonQueryExpression => nonQueryExpression,
4444
_ => base.VisitExtension(extensionExpression),
4545
};

src/EFCore.Relational/Query/QuerySqlGenerator.cs

+148-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Runtime.CompilerServices;
54
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
65
using Microsoft.EntityFrameworkCore.Storage.Internal;
76

@@ -169,15 +168,17 @@ protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragment
169168
}
170169

171170
private static bool IsNonComposedSetOperation(SelectExpression selectExpression)
172-
=> selectExpression.Offset == null
173-
&& selectExpression.Limit == null
174-
&& !selectExpression.IsDistinct
175-
&& selectExpression.Predicate == null
176-
&& selectExpression.Having == null
177-
&& selectExpression.Orderings.Count == 0
178-
&& selectExpression.GroupBy.Count == 0
179-
&& selectExpression.Tables.Count == 1
180-
&& selectExpression.Tables[0] is SetOperationBase setOperation
171+
=> selectExpression is
172+
{
173+
Tables: [SetOperationBase setOperation],
174+
Offset: null,
175+
Limit: null,
176+
IsDistinct: false,
177+
Predicate: null,
178+
Having: null,
179+
Orderings.Count: 0,
180+
GroupBy.Count: 0
181+
}
181182
&& selectExpression.Projection.Count == setOperation.Source1.Projection.Count
182183
&& selectExpression.Projection.Select(
183184
(pe, index) => pe.Expression is ColumnExpression column
@@ -226,12 +227,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
226227
subQueryIndent = _relationalCommandBuilder.Indent();
227228
}
228229

229-
if (IsNonComposedSetOperation(selectExpression))
230-
{
231-
// Naked set operation
232-
GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
233-
}
234-
else
230+
if (!TryGenerateWithoutWrappingSelect(selectExpression))
235231
{
236232
_relationalCommandBuilder.Append("SELECT ");
237233

@@ -300,6 +296,39 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
300296
return selectExpression;
301297
}
302298

299+
/// <summary>
300+
/// If possible, generates the expression contained within the provided <paramref name="selectExpression" /> without the wrapping
301+
/// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped
302+
/// in SELECT.
303+
/// </summary>
304+
protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
305+
{
306+
if (IsNonComposedSetOperation(selectExpression))
307+
{
308+
// Naked set operation
309+
GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
310+
return true;
311+
}
312+
313+
if (selectExpression is
314+
{
315+
Tables: [ValuesExpression valuesExpression],
316+
Offset: null,
317+
Limit: null,
318+
IsDistinct: false,
319+
Predicate: null,
320+
Having: null,
321+
Orderings.Count: 0,
322+
GroupBy.Count: 0,
323+
})
324+
{
325+
GenerateValues(valuesExpression, withParentheses: false);
326+
return true;
327+
}
328+
329+
return false;
330+
}
331+
303332
/// <summary>
304333
/// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause.
305334
/// </summary>
@@ -312,9 +341,7 @@ protected virtual void GeneratePseudoFromClause()
312341
/// </summary>
313342
/// <param name="selectExpression">SelectExpression for which the empty projection will be generated.</param>
314343
protected virtual void GenerateEmptyProjection(SelectExpression selectExpression)
315-
{
316-
_relationalCommandBuilder.Append("1");
317-
}
344+
=> _relationalCommandBuilder.Append("1");
318345

319346
/// <inheritdoc />
320347
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
@@ -371,16 +398,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
371398
/// <inheritdoc />
372399
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
373400
{
374-
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
401+
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
375402
{
376403
_relationalCommandBuilder
377-
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
404+
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
378405
.Append(".");
379406
}
380407

381-
var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
382-
? tableValuedFunctionExpression.StoreFunction.Name
383-
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
408+
var name = tableValuedFunctionExpression.IsBuiltIn
409+
? tableValuedFunctionExpression.Name
410+
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);
384411

385412
_relationalCommandBuilder
386413
.Append(name)
@@ -1132,6 +1159,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
11321159
return rowNumberExpression;
11331160
}
11341161

1162+
/// <inheritdoc />
1163+
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
1164+
{
1165+
Sql.Append("(");
1166+
1167+
var values = rowValueExpression.Values;
1168+
var count = values.Count;
1169+
for (var i = 0; i < count; i++)
1170+
{
1171+
if (i > 0)
1172+
{
1173+
Sql.Append(", ");
1174+
}
1175+
1176+
Visit(values[i]);
1177+
}
1178+
1179+
Sql.Append(")");
1180+
1181+
return rowValueExpression;
1182+
}
1183+
11351184
/// <summary>
11361185
/// Generates a set operation in the relational command.
11371186
/// </summary>
@@ -1141,18 +1190,16 @@ protected virtual void GenerateSetOperation(SetOperationBase setOperation)
11411190
GenerateSetOperationOperand(setOperation, setOperation.Source1);
11421191
_relationalCommandBuilder
11431192
.AppendLine()
1144-
.Append(GetSetOperation(setOperation))
1193+
.Append(
1194+
setOperation switch
1195+
{
1196+
ExceptExpression => "EXCEPT",
1197+
IntersectExpression => "INTERSECT",
1198+
UnionExpression => "UNION",
1199+
_ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
1200+
})
11451201
.AppendLine(setOperation.IsDistinct ? string.Empty : " ALL");
11461202
GenerateSetOperationOperand(setOperation, setOperation.Source2);
1147-
1148-
static string GetSetOperation(SetOperationBase operation)
1149-
=> operation switch
1150-
{
1151-
ExceptExpression => "EXCEPT",
1152-
IntersectExpression => "INTERSECT",
1153-
UnionExpression => "UNION",
1154-
_ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
1155-
};
11561203
}
11571204

11581205
/// <summary>
@@ -1311,6 +1358,73 @@ void LiftPredicate(TableExpressionBase joinTable)
13111358
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
13121359
}
13131360

1361+
/// <inheritdoc />
1362+
protected override Expression VisitValues(ValuesExpression valuesExpression)
1363+
{
1364+
GenerateValues(valuesExpression, withParentheses: true);
1365+
1366+
return valuesExpression;
1367+
}
1368+
1369+
/// <summary>
1370+
/// Generates a VALUES expression.
1371+
/// </summary>
1372+
protected virtual void GenerateValues(ValuesExpression valuesExpression, bool withParentheses = true)
1373+
{
1374+
// TODO: Review this!
1375+
if (withParentheses && valuesExpression.Alias is not null)
1376+
{
1377+
_relationalCommandBuilder.Append("(");
1378+
}
1379+
1380+
var rowValues = valuesExpression.RowValues;
1381+
1382+
// Some databases support providing the names of columns projected out of VALUES, e.g.
1383+
// SQL Server/PG: (VALUES (1, 3), (2, 4)) AS x(a, b). Others unfortunately don't; so by default, we extract out the first row,
1384+
// and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
1385+
_relationalCommandBuilder.Append("SELECT ");
1386+
1387+
Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
1388+
var firstRowValues = rowValues[0].Values;
1389+
for (var i = 0; i < firstRowValues.Count; i++)
1390+
{
1391+
if (i > 0)
1392+
{
1393+
_relationalCommandBuilder.Append(", ");
1394+
}
1395+
1396+
Visit(firstRowValues[i]);
1397+
1398+
_relationalCommandBuilder
1399+
.Append(AliasSeparator)
1400+
.Append(valuesExpression.ColumnNames[i]);
1401+
}
1402+
1403+
if (rowValues.Count > 1)
1404+
{
1405+
_relationalCommandBuilder.Append(" UNION ALL VALUES ");
1406+
1407+
for (var i = 1; i < rowValues.Count; i++)
1408+
{
1409+
// TODO: Do we want newlines here?
1410+
if (i > 1)
1411+
{
1412+
_relationalCommandBuilder.Append(", ");
1413+
}
1414+
1415+
Visit(valuesExpression.RowValues[i]);
1416+
}
1417+
}
1418+
1419+
if (withParentheses && valuesExpression.Alias is not null)
1420+
{
1421+
_relationalCommandBuilder
1422+
.Append(")")
1423+
.Append(AliasSeparator)
1424+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
1425+
}
1426+
}
1427+
13141428
/// <inheritdoc />
13151429
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
13161430
=> throw new InvalidOperationException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.Query.Internal;
5+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
6+
7+
namespace Microsoft.EntityFrameworkCore.Query;
8+
9+
/// <inheritdoc />
10+
public class RelationalQueryRootProcessor : QueryRootProcessor
11+
{
12+
private readonly ITypeMappingSource _typeMappingSource;
13+
private readonly IModel _model;
14+
15+
/// <summary>
16+
/// Creates a new instance of the <see cref="RelationalQueryRootProcessor" /> class.
17+
/// </summary>
18+
/// <param name="typeMappingSource">The type mapping source.</param>
19+
/// <param name="model">The model.</param>
20+
public RelationalQueryRootProcessor(ITypeMappingSource typeMappingSource, IModel model)
21+
{
22+
_typeMappingSource = typeMappingSource;
23+
_model = model;
24+
}
25+
26+
/// <inheritdoc />
27+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
28+
{
29+
// Create query root node for table-valued functions
30+
if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction })
31+
{
32+
// See issue #19970
33+
return new TableValuedFunctionQueryRootExpression(
34+
storeFunction.EntityTypeMappings.Single().EntityType,
35+
storeFunction,
36+
methodCallExpression.Arguments);
37+
}
38+
39+
return base.VisitMethodCall(methodCallExpression);
40+
}
41+
42+
/// <summary>
43+
/// Given a queryable constants over an element type which has a type mapping, converts it to a
44+
/// <see cref="ConstantQueryRootExpression" />; it will be translated to a SQL <see cref="ValuesExpression" />.
45+
/// </summary>
46+
/// <param name="constantExpression">The constant expression to attempt to convert to a query root.</param>
47+
protected override Expression VisitQueryableConstant(ConstantExpression constantExpression)
48+
// TODO: Note that we restrict to constants whose element type is mappable as-is. This excludes a constant list with an unsupported
49+
// CLR type, where we could infer a type mapping with a value converter based on its later usage. However, converting all constant
50+
// collections to query roots also carries risk.
51+
=> constantExpression.Type.TryGetSequenceType() is Type elementType && _typeMappingSource.FindMapping(elementType) is not null
52+
? new ConstantQueryRootExpression(constantExpression)
53+
: constantExpression;
54+
55+
/// <inheritdoc />
56+
protected override Expression VisitQueryableParameter(ParameterExpression parameterExpression)
57+
{
58+
// TODO: Decide whether this belongs here or in specific provider code. This means parameter query roots always get created
59+
// (in enumerable/queryable context), but may not be translatable in the provider's QueryableMethodTranslatingEV. As long
60+
// as we unwrap there, we *should* be OK, and so don't need an additional provider extension point here...
61+
62+
// TODO: Also, maybe this type checking should be in the base class.
63+
// SQL Server's OpenJson, which we use to unpack the queryable parameter, does not support geometry (or any other non-built-in
64+
// types)
65+
66+
// We convert to query roots only parameters whose CLR type has a collection type mapping (i.e. ElementTypeMapping isn't null).
67+
// This allows the provider to determine exactly which types are supported as queryable collections (e.g. OPENJSON on SQL Server).
68+
return _typeMappingSource.FindMapping(parameterExpression.Type) is { ElementTypeMapping: not null }
69+
? new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression)
70+
: parameterExpression;
71+
}
72+
73+
/// <inheritdoc />
74+
protected override Expression VisitExtension(Expression node)
75+
=> node switch
76+
{
77+
// We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
78+
// that to a query root
79+
FromSqlQueryRootExpression e => e,
80+
81+
_ => base.VisitExtension(node)
82+
};
83+
84+
}

0 commit comments

Comments
 (0)