Skip to content

Commit 75a8d4f

Browse files
committed
Support primitive collections
Closes dotnet#29427 Closes dotnet#30426 Closes dotnet#13617
1 parent 24de85c commit 75a8d4f

File tree

96 files changed

+8180
-530
lines changed

Some content is hidden

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

96 files changed

+8180
-530
lines changed

All.sln.DotSettings

+3
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,12 @@ 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>
316+
<s:Boolean x:Key="/Default/UserDictionary/Words/=queryables/@EntryIndexedValue">True</s:Boolean>
314317
<s:Boolean x:Key="/Default/UserDictionary/Words/=remapper/@EntryIndexedValue">True</s:Boolean>
315318
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>
316319
<s:Boolean x:Key="/Default/UserDictionary/Words/=retriable/@EntryIndexedValue">True</s:Boolean>

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

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

src/EFCore.Relational/Properties/RelationalStrings.resx

+9
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@
175175
<data name="ConflictingRowValuesSensitive" xml:space="preserve">
176176
<value>Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.</value>
177177
</data>
178+
<data name="ConflictingTypeMappingsForPrimitiveCollection" xml:space="preserve">
179+
<value>Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.</value>
180+
</data>
178181
<data name="ConflictingSeedValues" xml:space="preserve">
179182
<value>A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.</value>
180183
</data>
@@ -346,6 +349,9 @@
346349
<data name="EitherOfTwoValuesMustBeNull" xml:space="preserve">
347350
<value>Either {param1} or {param2} must be null.</value>
348351
</data>
352+
<data name="EmptyCollectionNotSupportedAsInlineQueryRoot" xml:space="preserve">
353+
<value>Empty collections are not supported as inline query roots.</value>
354+
</data>
349355
<data name="EntityShortNameNotUnique" xml:space="preserve">
350356
<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>
351357
</data>
@@ -950,6 +956,9 @@
950956
<data name="OptionalDependentWithDependentWithoutIdentifyingProperty" xml:space="preserve">
951957
<value>Entity type '{entityType}' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.</value>
952958
</data>
959+
<data name="OnlyConstantsSupportedInInlineCollectionQueryRoots" xml:space="preserve">
960+
<value>Only constants are supported inside inline collection query roots.</value>
961+
</data>
953962
<data name="ParameterNotObjectArray" xml:space="preserve">
954963
<value>Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].</value>
955964
</data>

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
6060
&& right != null)
6161
{
6262
if (left.Type == right.Type
63-
|| (right.Type == typeof(object) && (right is SqlParameterExpression || right is SqlConstantExpression))
64-
|| (left.Type == typeof(object) && (left is SqlParameterExpression || left is SqlConstantExpression)))
63+
|| (right.Type == typeof(object)
64+
&& right is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null })
65+
|| (left.Type == typeof(object)
66+
&& left is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null }))
6567
{
6668
return _sqlExpressionFactory.Equal(left, right);
6769
}

src/EFCore.Relational/Query/QuerySqlGenerator.cs

+132-16
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
226226
subQueryIndent = _relationalCommandBuilder.Indent();
227227
}
228228

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

@@ -300,6 +295,43 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
300295
return selectExpression;
301296
}
302297

298+
/// <summary>
299+
/// If possible, generates the expression contained within the provided <paramref name="selectExpression" /> without the wrapping
300+
/// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped
301+
/// in SELECT.
302+
/// </summary>
303+
protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
304+
{
305+
if (IsNonComposedSetOperation(selectExpression))
306+
{
307+
GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
308+
return true;
309+
}
310+
311+
if (selectExpression is
312+
{
313+
Tables: [ValuesExpression valuesExpression],
314+
Offset: null,
315+
Limit: null,
316+
IsDistinct: false,
317+
Predicate: null,
318+
Having: null,
319+
Orderings.Count: 0,
320+
GroupBy.Count: 0,
321+
}
322+
&& selectExpression.Projection.Count == valuesExpression.ColumnNames.Count
323+
&& selectExpression.Projection.Select(
324+
(pe, index) => pe.Expression is ColumnExpression column
325+
&& column.Name == valuesExpression.ColumnNames[index])
326+
.All(e => e))
327+
{
328+
GenerateValues(valuesExpression);
329+
return true;
330+
}
331+
332+
return false;
333+
}
334+
303335
/// <summary>
304336
/// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause.
305337
/// </summary>
@@ -371,16 +403,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
371403
/// <inheritdoc />
372404
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
373405
{
374-
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
406+
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
375407
{
376408
_relationalCommandBuilder
377-
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
409+
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
378410
.Append(".");
379411
}
380412

381-
var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
382-
? tableValuedFunctionExpression.StoreFunction.Name
383-
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
413+
var name = tableValuedFunctionExpression.IsBuiltIn
414+
? tableValuedFunctionExpression.Name
415+
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);
384416

385417
_relationalCommandBuilder
386418
.Append(name)
@@ -607,19 +639,22 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
607639
{
608640
var invariantName = sqlParameterExpression.Name;
609641
var parameterName = sqlParameterExpression.Name;
642+
var typeMapping = sqlParameterExpression.TypeMapping!;
610643

611644
// Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
612645
// data twice.
613646
// Note that if the type mapping differs, we do send the same data twice (e.g. the same string may be sent once as Unicode, once as
614647
// non-Unicode).
648+
// TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for
649+
// that we need to ensure that there's only ever one type mapping instance (i.e. no type mappings are ever instantiated out of the
650+
// type mapping source). See #30677.
615651
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
616652
p =>
617653
p.InvariantName == parameterName
618-
&& p is TypeMappedRelationalParameter typeMappedRelationalParameter
619-
&& string.Equals(
620-
typeMappedRelationalParameter.RelationalTypeMapping.StoreType, sqlParameterExpression.TypeMapping!.StoreType,
621-
StringComparison.OrdinalIgnoreCase)
622-
&& typeMappedRelationalParameter.RelationalTypeMapping.Converter == sqlParameterExpression.TypeMapping!.Converter);
654+
&& p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
655+
&& string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
656+
&& (existingTypeMapping.Converter is null && typeMapping.Converter is null
657+
|| existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));
623658

624659
if (parameter is null)
625660
{
@@ -1132,6 +1167,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
11321167
return rowNumberExpression;
11331168
}
11341169

1170+
/// <inheritdoc />
1171+
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
1172+
{
1173+
Sql.Append("(");
1174+
1175+
var values = rowValueExpression.Values;
1176+
var count = values.Count;
1177+
for (var i = 0; i < count; i++)
1178+
{
1179+
if (i > 0)
1180+
{
1181+
Sql.Append(", ");
1182+
}
1183+
1184+
Visit(values[i]);
1185+
}
1186+
1187+
Sql.Append(")");
1188+
1189+
return rowValueExpression;
1190+
}
1191+
11351192
/// <summary>
11361193
/// Generates a set operation in the relational command.
11371194
/// </summary>
@@ -1311,6 +1368,65 @@ void LiftPredicate(TableExpressionBase joinTable)
13111368
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
13121369
}
13131370

1371+
/// <inheritdoc />
1372+
protected override Expression VisitValues(ValuesExpression valuesExpression)
1373+
{
1374+
_relationalCommandBuilder.Append("(");
1375+
1376+
GenerateValues(valuesExpression);
1377+
1378+
_relationalCommandBuilder
1379+
.Append(")")
1380+
.Append(AliasSeparator)
1381+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
1382+
1383+
return valuesExpression;
1384+
}
1385+
1386+
/// <summary>
1387+
/// Generates a VALUES expression.
1388+
/// </summary>
1389+
protected virtual void GenerateValues(ValuesExpression valuesExpression)
1390+
{
1391+
var rowValues = valuesExpression.RowValues;
1392+
1393+
// Some databases support providing the names of columns projected out of VALUES, e.g.
1394+
// 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,
1395+
// and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
1396+
_relationalCommandBuilder.Append("SELECT ");
1397+
1398+
Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
1399+
var firstRowValues = rowValues[0].Values;
1400+
for (var i = 0; i < firstRowValues.Count; i++)
1401+
{
1402+
if (i > 0)
1403+
{
1404+
_relationalCommandBuilder.Append(", ");
1405+
}
1406+
1407+
Visit(firstRowValues[i]);
1408+
1409+
_relationalCommandBuilder
1410+
.Append(AliasSeparator)
1411+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
1412+
}
1413+
1414+
if (rowValues.Count > 1)
1415+
{
1416+
_relationalCommandBuilder.Append(" UNION ALL VALUES ");
1417+
1418+
for (var i = 1; i < rowValues.Count; i++)
1419+
{
1420+
if (i > 1)
1421+
{
1422+
_relationalCommandBuilder.Append(", ");
1423+
}
1424+
1425+
Visit(valuesExpression.RowValues[i]);
1426+
}
1427+
}
1428+
}
1429+
13141430
/// <inheritdoc />
13151431
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
13161432
=> throw new InvalidOperationException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 IModel _model;
13+
14+
/// <summary>
15+
/// Creates a new instance of the <see cref="RelationalQueryRootProcessor" /> class.
16+
/// </summary>
17+
/// <param name="dependencies">Parameter object containing dependencies for this class.</param>
18+
/// <param name="relationalDependencies">Parameter object containing relational dependencies for this class.</param>
19+
/// <param name="queryCompilationContext">The query compilation context object to use.</param>
20+
public RelationalQueryRootProcessor(
21+
QueryTranslationPreprocessorDependencies dependencies,
22+
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
23+
QueryCompilationContext queryCompilationContext)
24+
: base(dependencies, queryCompilationContext)
25+
{
26+
_model = queryCompilationContext.Model;
27+
}
28+
29+
/// <summary>
30+
/// Indicates that a <see cref="ConstantExpression" /> can be converted to a <see cref="InlineQueryRootExpression" />;
31+
/// this will later be translated to a SQL <see cref="ValuesExpression" />.
32+
/// </summary>
33+
protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
34+
=> true;
35+
36+
/// <inheritdoc />
37+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
38+
{
39+
// Create query root node for table-valued functions
40+
if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction })
41+
{
42+
// See issue #19970
43+
return new TableValuedFunctionQueryRootExpression(
44+
storeFunction.EntityTypeMappings.Single().EntityType,
45+
storeFunction,
46+
methodCallExpression.Arguments);
47+
}
48+
49+
return base.VisitMethodCall(methodCallExpression);
50+
}
51+
52+
/// <inheritdoc />
53+
protected override Expression VisitExtension(Expression node)
54+
=> node switch
55+
{
56+
// We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
57+
// that to a query root
58+
FromSqlQueryRootExpression e => e,
59+
60+
_ => base.VisitExtension(node)
61+
};
62+
}

0 commit comments

Comments
 (0)