Skip to content

Commit 3076b20

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

File tree

90 files changed

+5952
-596
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

+5952
-596
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

+152-42
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,21 +168,22 @@ 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
184-
&& string.Equals(column.TableAlias, setOperation.Alias, StringComparison.Ordinal)
185-
&& string.Equals(
186-
column.Name, setOperation.Source1.Projection[index].Alias, StringComparison.Ordinal))
185+
&& column.TableAlias == setOperation.Alias
186+
&& column.Name == setOperation.Source1.Projection[index].Alias)
187187
.All(e => e);
188188

189189
/// <inheritdoc />
@@ -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>
@@ -312,9 +344,7 @@ protected virtual void GeneratePseudoFromClause()
312344
/// </summary>
313345
/// <param name="selectExpression">SelectExpression for which the empty projection will be generated.</param>
314346
protected virtual void GenerateEmptyProjection(SelectExpression selectExpression)
315-
{
316-
_relationalCommandBuilder.Append("1");
317-
}
347+
=> _relationalCommandBuilder.Append("1");
318348

319349
/// <inheritdoc />
320350
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
@@ -371,16 +401,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
371401
/// <inheritdoc />
372402
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
373403
{
374-
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
404+
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
375405
{
376406
_relationalCommandBuilder
377-
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
407+
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
378408
.Append(".");
379409
}
380410

381-
var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
382-
? tableValuedFunctionExpression.StoreFunction.Name
383-
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
411+
var name = tableValuedFunctionExpression.IsBuiltIn
412+
? tableValuedFunctionExpression.Name
413+
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);
384414

385415
_relationalCommandBuilder
386416
.Append(name)
@@ -607,6 +637,7 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
607637
{
608638
var invariantName = sqlParameterExpression.Name;
609639
var parameterName = sqlParameterExpression.Name;
640+
var typeMapping = sqlParameterExpression.TypeMapping!;
610641

611642
// Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
612643
// data twice.
@@ -615,11 +646,10 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
615646
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
616647
p =>
617648
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);
649+
&& p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
650+
&& string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
651+
&& (existingTypeMapping.Converter is null && typeMapping.Converter is null
652+
|| existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));
623653

624654
if (parameter is null)
625655
{
@@ -1132,6 +1162,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
11321162
return rowNumberExpression;
11331163
}
11341164

1165+
/// <inheritdoc />
1166+
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
1167+
{
1168+
Sql.Append("(");
1169+
1170+
var values = rowValueExpression.Values;
1171+
var count = values.Count;
1172+
for (var i = 0; i < count; i++)
1173+
{
1174+
if (i > 0)
1175+
{
1176+
Sql.Append(", ");
1177+
}
1178+
1179+
Visit(values[i]);
1180+
}
1181+
1182+
Sql.Append(")");
1183+
1184+
return rowValueExpression;
1185+
}
1186+
11351187
/// <summary>
11361188
/// Generates a set operation in the relational command.
11371189
/// </summary>
@@ -1141,18 +1193,16 @@ protected virtual void GenerateSetOperation(SetOperationBase setOperation)
11411193
GenerateSetOperationOperand(setOperation, setOperation.Source1);
11421194
_relationalCommandBuilder
11431195
.AppendLine()
1144-
.Append(GetSetOperation(setOperation))
1196+
.Append(
1197+
setOperation switch
1198+
{
1199+
ExceptExpression => "EXCEPT",
1200+
IntersectExpression => "INTERSECT",
1201+
UnionExpression => "UNION",
1202+
_ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
1203+
})
11451204
.AppendLine(setOperation.IsDistinct ? string.Empty : " ALL");
11461205
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-
};
11561206
}
11571207

11581208
/// <summary>
@@ -1311,6 +1361,66 @@ void LiftPredicate(TableExpressionBase joinTable)
13111361
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
13121362
}
13131363

1364+
/// <inheritdoc />
1365+
protected override Expression VisitValues(ValuesExpression valuesExpression)
1366+
{
1367+
_relationalCommandBuilder.Append("(");
1368+
1369+
GenerateValues(valuesExpression);
1370+
1371+
_relationalCommandBuilder
1372+
.Append(")")
1373+
.Append(AliasSeparator)
1374+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
1375+
1376+
return valuesExpression;
1377+
}
1378+
1379+
/// <summary>
1380+
/// Generates a VALUES expression.
1381+
/// </summary>
1382+
protected virtual void GenerateValues(ValuesExpression valuesExpression)
1383+
{
1384+
var rowValues = valuesExpression.RowValues;
1385+
1386+
// Some databases support providing the names of columns projected out of VALUES, e.g.
1387+
// 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,
1388+
// and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
1389+
_relationalCommandBuilder.Append("SELECT ");
1390+
1391+
Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
1392+
var firstRowValues = rowValues[0].Values;
1393+
for (var i = 0; i < firstRowValues.Count; i++)
1394+
{
1395+
if (i > 0)
1396+
{
1397+
_relationalCommandBuilder.Append(", ");
1398+
}
1399+
1400+
Visit(firstRowValues[i]);
1401+
1402+
_relationalCommandBuilder
1403+
.Append(AliasSeparator)
1404+
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
1405+
}
1406+
1407+
if (rowValues.Count > 1)
1408+
{
1409+
_relationalCommandBuilder.Append(" UNION ALL VALUES ");
1410+
1411+
for (var i = 1; i < rowValues.Count; i++)
1412+
{
1413+
// TODO: Do we want newlines here?
1414+
if (i > 1)
1415+
{
1416+
_relationalCommandBuilder.Append(", ");
1417+
}
1418+
1419+
Visit(valuesExpression.RowValues[i]);
1420+
}
1421+
}
1422+
}
1423+
13141424
/// <inheritdoc />
13151425
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
13161426
=> throw new InvalidOperationException(

0 commit comments

Comments
 (0)