Skip to content

Commit 728acbb

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

File tree

82 files changed

+6430
-463
lines changed

Some content is hidden

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

82 files changed

+6430
-463
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

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

src/EFCore.Relational/Properties/RelationalStrings.resx

+6
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="EmptyCollectionNotSupportedAsConstantQueryRoot" xml:space="preserve">
353+
<value>Empty constant collections are not supported as constant 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>

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

-47
This file was deleted.

src/EFCore.Relational/Query/QuerySqlGenerator.cs

+130-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,6 +639,7 @@ 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.
@@ -615,11 +648,10 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
615648
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
616649
p =>
617650
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);
651+
&& p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
652+
&& string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
653+
&& (existingTypeMapping.Converter is null && typeMapping.Converter is null
654+
|| existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));
623655

624656
if (parameter is null)
625657
{
@@ -1132,6 +1164,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
11321164
return rowNumberExpression;
11331165
}
11341166

1167+
/// <inheritdoc />
1168+
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
1169+
{
1170+
Sql.Append("(");
1171+
1172+
var values = rowValueExpression.Values;
1173+
var count = values.Count;
1174+
for (var i = 0; i < count; i++)
1175+
{
1176+
if (i > 0)
1177+
{
1178+
Sql.Append(", ");
1179+
}
1180+
1181+
Visit(values[i]);
1182+
}
1183+
1184+
Sql.Append(")");
1185+
1186+
return rowValueExpression;
1187+
}
1188+
11351189
/// <summary>
11361190
/// Generates a set operation in the relational command.
11371191
/// </summary>
@@ -1311,6 +1365,66 @@ void LiftPredicate(TableExpressionBase joinTable)
13111365
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
13121366
}
13131367

1368+
/// <inheritdoc />
1369+
protected override Expression VisitValues(ValuesExpression valuesExpression)
1370+
{
1371+
_relationalCommandBuilder.Append("(");
1372+
1373+
GenerateValues(valuesExpression);
1374+
1375+
_relationalCommandBuilder
1376+
.Append(")")
1377+
.Append(AliasSeparator)
1378+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
1379+
1380+
return valuesExpression;
1381+
}
1382+
1383+
/// <summary>
1384+
/// Generates a VALUES expression.
1385+
/// </summary>
1386+
protected virtual void GenerateValues(ValuesExpression valuesExpression)
1387+
{
1388+
var rowValues = valuesExpression.RowValues;
1389+
1390+
// Some databases support providing the names of columns projected out of VALUES, e.g.
1391+
// 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,
1392+
// and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
1393+
_relationalCommandBuilder.Append("SELECT ");
1394+
1395+
Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
1396+
var firstRowValues = rowValues[0].Values;
1397+
for (var i = 0; i < firstRowValues.Count; i++)
1398+
{
1399+
if (i > 0)
1400+
{
1401+
_relationalCommandBuilder.Append(", ");
1402+
}
1403+
1404+
Visit(firstRowValues[i]);
1405+
1406+
_relationalCommandBuilder
1407+
.Append(AliasSeparator)
1408+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
1409+
}
1410+
1411+
if (rowValues.Count > 1)
1412+
{
1413+
_relationalCommandBuilder.Append(" UNION ALL VALUES ");
1414+
1415+
for (var i = 1; i < rowValues.Count; i++)
1416+
{
1417+
// TODO: Do we want newlines here?
1418+
if (i > 1)
1419+
{
1420+
_relationalCommandBuilder.Append(", ");
1421+
}
1422+
1423+
Visit(valuesExpression.RowValues[i]);
1424+
}
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,73 @@
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+
// We convert to query roots only parameters whose CLR type has a collection type mapping (i.e. ElementTypeMapping isn't null).
58+
// This allows the provider to determine exactly which types are supported as queryable collections (e.g. OPENJSON on SQL Server).
59+
=> _typeMappingSource.FindMapping(parameterExpression.Type) is { ElementTypeMapping: not null }
60+
? new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression)
61+
: parameterExpression;
62+
63+
/// <inheritdoc />
64+
protected override Expression VisitExtension(Expression node)
65+
=> node switch
66+
{
67+
// We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
68+
// that to a query root
69+
FromSqlQueryRootExpression e => e,
70+
71+
_ => base.VisitExtension(node)
72+
};
73+
}

0 commit comments

Comments
 (0)