Skip to content

Commit

Permalink
Support primitive collections
Browse files Browse the repository at this point in the history
Closes #29427
Closes #30426
Closes #13617
  • Loading branch information
roji committed Apr 26, 2023
1 parent 309d8f0 commit c0f9164
Show file tree
Hide file tree
Showing 103 changed files with 8,539 additions and 638 deletions.
3 changes: 3 additions & 0 deletions All.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,12 @@ The .NET Foundation licenses this file to you under the MIT license.
<s:Boolean x:Key="/Default/UserDictionary/Words/=navigations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=niladic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=NOCASE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OPENJSON/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=queryables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=remapper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=retriable/@EntryIndexedValue">True</s:Boolean>
Expand Down
22 changes: 21 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@
<data name="ConflictingRowValuesSensitive" xml:space="preserve">
<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>
</data>
<data name="ConflictingTypeMappingsForPrimitiveCollection" xml:space="preserve">
<value>Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.</value>
</data>
<data name="ConflictingSeedValues" xml:space="preserve">
<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>
</data>
Expand Down Expand Up @@ -346,6 +349,9 @@
<data name="EitherOfTwoValuesMustBeNull" xml:space="preserve">
<value>Either {param1} or {param2} must be null.</value>
</data>
<data name="EmptyCollectionNotSupportedAsInlineQueryRoot" xml:space="preserve">
<value>Empty collections are not supported as inline query roots.</value>
</data>
<data name="EntityShortNameNotUnique" xml:space="preserve">
<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>
</data>
Expand Down Expand Up @@ -950,6 +956,9 @@
<data name="OptionalDependentWithDependentWithoutIdentifyingProperty" xml:space="preserve">
<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>
</data>
<data name="OnlyConstantsSupportedInInlineCollectionQueryRoots" xml:space="preserve">
<value>Only constants are supported inside inline collection query roots.</value>
</data>
<data name="ParameterNotObjectArray" xml:space="preserve">
<value>Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].</value>
</data>
Expand Down
6 changes: 4 additions & 2 deletions src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
&& right != null)
{
if (left.Type == right.Type
|| (right.Type == typeof(object) && (right is SqlParameterExpression || right is SqlConstantExpression))
|| (left.Type == typeof(object) && (left is SqlParameterExpression || left is SqlConstantExpression)))
|| (right.Type == typeof(object)
&& right is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null })
|| (left.Type == typeof(object)
&& left is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null }))
{
return _sqlExpressionFactory.Equal(left, right);
}
Expand Down
148 changes: 132 additions & 16 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
subQueryIndent = _relationalCommandBuilder.Indent();
}

if (IsNonComposedSetOperation(selectExpression))
{
// Naked set operation
GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
}
else
if (!TryGenerateWithoutWrappingSelect(selectExpression))
{
_relationalCommandBuilder.Append("SELECT ");

Expand Down Expand Up @@ -300,6 +295,43 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
return selectExpression;
}

/// <summary>
/// If possible, generates the expression contained within the provided <paramref name="selectExpression" /> without the wrapping
/// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped
/// in SELECT.
/// </summary>
protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
{
if (IsNonComposedSetOperation(selectExpression))
{
GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
return true;
}

if (selectExpression is
{
Tables: [ValuesExpression valuesExpression],
Offset: null,
Limit: null,
IsDistinct: false,
Predicate: null,
Having: null,
Orderings.Count: 0,
GroupBy.Count: 0,
}
&& selectExpression.Projection.Count == valuesExpression.ColumnNames.Count
&& selectExpression.Projection.Select(
(pe, index) => pe.Expression is ColumnExpression column
&& column.Name == valuesExpression.ColumnNames[index])
.All(e => e))
{
GenerateValues(valuesExpression);
return true;
}

return false;
}

/// <summary>
/// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause.
/// </summary>
Expand Down Expand Up @@ -371,16 +403,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
/// <inheritdoc />
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
{
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
{
_relationalCommandBuilder
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
.Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
.Append(".");
}

var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
? tableValuedFunctionExpression.StoreFunction.Name
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
var name = tableValuedFunctionExpression.IsBuiltIn
? tableValuedFunctionExpression.Name
: _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);

_relationalCommandBuilder
.Append(name)
Expand Down Expand Up @@ -607,19 +639,22 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
{
var invariantName = sqlParameterExpression.Name;
var parameterName = sqlParameterExpression.Name;
var typeMapping = sqlParameterExpression.TypeMapping!;

// Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
// data twice.
// 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
// non-Unicode).
// TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for
// 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
// type mapping source). See #30677.
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
p =>
p.InvariantName == parameterName
&& p is TypeMappedRelationalParameter typeMappedRelationalParameter
&& string.Equals(
typeMappedRelationalParameter.RelationalTypeMapping.StoreType, sqlParameterExpression.TypeMapping!.StoreType,
StringComparison.OrdinalIgnoreCase)
&& typeMappedRelationalParameter.RelationalTypeMapping.Converter == sqlParameterExpression.TypeMapping!.Converter);
&& p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
&& string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
&& (existingTypeMapping.Converter is null && typeMapping.Converter is null
|| existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));

if (parameter is null)
{
Expand Down Expand Up @@ -1132,6 +1167,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
return rowNumberExpression;
}

/// <inheritdoc />
protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
{
Sql.Append("(");

var values = rowValueExpression.Values;
var count = values.Count;
for (var i = 0; i < count; i++)
{
if (i > 0)
{
Sql.Append(", ");
}

Visit(values[i]);
}

Sql.Append(")");

return rowValueExpression;
}

/// <summary>
/// Generates a set operation in the relational command.
/// </summary>
Expand Down Expand Up @@ -1311,6 +1368,65 @@ void LiftPredicate(TableExpressionBase joinTable)
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
}

/// <inheritdoc />
protected override Expression VisitValues(ValuesExpression valuesExpression)
{
_relationalCommandBuilder.Append("(");

GenerateValues(valuesExpression);

_relationalCommandBuilder
.Append(")")
.Append(AliasSeparator)
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));

return valuesExpression;
}

/// <summary>
/// Generates a VALUES expression.
/// </summary>
protected virtual void GenerateValues(ValuesExpression valuesExpression)
{
var rowValues = valuesExpression.RowValues;

// Some databases support providing the names of columns projected out of VALUES, e.g.
// 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,
// and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
_relationalCommandBuilder.Append("SELECT ");

Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
var firstRowValues = rowValues[0].Values;
for (var i = 0; i < firstRowValues.Count; i++)
{
if (i > 0)
{
_relationalCommandBuilder.Append(", ");
}

Visit(firstRowValues[i]);

_relationalCommandBuilder
.Append(AliasSeparator)
.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
}

if (rowValues.Count > 1)
{
_relationalCommandBuilder.Append(" UNION ALL VALUES ");

for (var i = 1; i < rowValues.Count; i++)
{
if (i > 1)
{
_relationalCommandBuilder.Append(", ");
}

Visit(valuesExpression.RowValues[i]);
}
}
}

/// <inheritdoc />
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
=> throw new InvalidOperationException(
Expand Down
62 changes: 62 additions & 0 deletions src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query;

/// <inheritdoc />
public class RelationalQueryRootProcessor : QueryRootProcessor
{
private readonly IModel _model;

/// <summary>
/// Creates a new instance of the <see cref="RelationalQueryRootProcessor" /> class.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this class.</param>
/// <param name="relationalDependencies">Parameter object containing relational dependencies for this class.</param>
/// <param name="queryCompilationContext">The query compilation context object to use.</param>
public RelationalQueryRootProcessor(
QueryTranslationPreprocessorDependencies dependencies,
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, queryCompilationContext)
{
_model = queryCompilationContext.Model;
}

/// <summary>
/// Indicates that a <see cref="ConstantExpression" /> can be converted to a <see cref="InlineQueryRootExpression" />;
/// this will later be translated to a SQL <see cref="ValuesExpression" />.
/// </summary>
protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
=> true;

/// <inheritdoc />
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
// Create query root node for table-valued functions
if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction })
{
// See issue #19970
return new TableValuedFunctionQueryRootExpression(
storeFunction.EntityTypeMappings.Single().EntityType,
storeFunction,
methodCallExpression.Arguments);
}

return base.VisitMethodCall(methodCallExpression);
}

/// <inheritdoc />
protected override Expression VisitExtension(Expression node)
=> node switch
{
// We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
// that to a query root
FromSqlQueryRootExpression e => e,

_ => base.VisitExtension(node)
};
}
Loading

0 comments on commit c0f9164

Please sign in to comment.