Skip to content

Commit

Permalink
Support primitive collections
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Apr 12, 2023
1 parent 55ec594 commit 97190f3
Show file tree
Hide file tree
Showing 93 changed files with 6,632 additions and 601 deletions.
2 changes: 2 additions & 0 deletions All.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,10 @@ 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/=remapper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Relational/Metadata/ITableBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ string SchemaQualifiedName
/// Gets the value indicating whether an entity of the given type might not be present in a row.
/// </summary>
bool IsOptional(IEntityType entityType);

/// <summary>
/// <para>
/// Creates a human-readable representation of the given metadata.
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

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

6 changes: 6 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="EmptyCollectionNotSupportedAsConstantQueryRoot" xml:space="preserve">
<value>Empty constant collections are not supported as constant 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
5 changes: 2 additions & 3 deletions src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,10 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
}

private static bool ValidateValues(SqlExpression values)
=> values is SqlConstantExpression || values is SqlParameterExpression;
=> values is SqlConstantExpression or SqlParameterExpression;

private static SqlExpression RemoveObjectConvert(SqlExpression expression)
=> expression is SqlUnaryExpression sqlUnaryExpression
&& sqlUnaryExpression.OperatorType == ExpressionType.Convert
=> expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression
&& sqlUnaryExpression.Type == typeof(object)
? sqlUnaryExpression.Operand
: expression;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public SelectExpressionProjectionApplyingExpressionVisitor(QuerySplittingBehavio
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
ShapedQueryExpression shapedQueryExpression
when shapedQueryExpression.QueryExpression is SelectExpression selectExpression
ShapedQueryExpression { QueryExpression: SelectExpression selectExpression } shapedQueryExpression
=> shapedQueryExpression.UpdateShaperExpression(
selectExpression.ApplyProjection(
shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior)),

NonQueryExpression nonQueryExpression => nonQueryExpression,
_ => base.VisitExtension(extensionExpression),
};
Expand Down
194 changes: 152 additions & 42 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage.Internal;

Expand Down Expand Up @@ -169,21 +168,22 @@ protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragment
}

private static bool IsNonComposedSetOperation(SelectExpression selectExpression)
=> selectExpression.Offset == null
&& selectExpression.Limit == null
&& !selectExpression.IsDistinct
&& selectExpression.Predicate == null
&& selectExpression.Having == null
&& selectExpression.Orderings.Count == 0
&& selectExpression.GroupBy.Count == 0
&& selectExpression.Tables.Count == 1
&& selectExpression.Tables[0] is SetOperationBase setOperation
=> selectExpression is
{
Tables: [SetOperationBase setOperation],
Offset: null,
Limit: null,
IsDistinct: false,
Predicate: null,
Having: null,
Orderings.Count: 0,
GroupBy.Count: 0
}
&& selectExpression.Projection.Count == setOperation.Source1.Projection.Count
&& selectExpression.Projection.Select(
(pe, index) => pe.Expression is ColumnExpression column
&& string.Equals(column.TableAlias, setOperation.Alias, StringComparison.Ordinal)
&& string.Equals(
column.Name, setOperation.Source1.Projection[index].Alias, StringComparison.Ordinal))
&& column.TableAlias == setOperation.Alias
&& column.Name == setOperation.Source1.Projection[index].Alias)
.All(e => e);

/// <inheritdoc />
Expand Down 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 All @@ -312,9 +344,7 @@ protected virtual void GeneratePseudoFromClause()
/// </summary>
/// <param name="selectExpression">SelectExpression for which the empty projection will be generated.</param>
protected virtual void GenerateEmptyProjection(SelectExpression selectExpression)
{
_relationalCommandBuilder.Append("1");
}
=> _relationalCommandBuilder.Append("1");

/// <inheritdoc />
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
Expand Down Expand Up @@ -371,16 +401,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,6 +637,7 @@ 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.
Expand All @@ -615,11 +646,10 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
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 +1162,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 All @@ -1141,18 +1193,16 @@ protected virtual void GenerateSetOperation(SetOperationBase setOperation)
GenerateSetOperationOperand(setOperation, setOperation.Source1);
_relationalCommandBuilder
.AppendLine()
.Append(GetSetOperation(setOperation))
.Append(
setOperation switch
{
ExceptExpression => "EXCEPT",
IntersectExpression => "INTERSECT",
UnionExpression => "UNION",
_ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
})
.AppendLine(setOperation.IsDistinct ? string.Empty : " ALL");
GenerateSetOperationOperand(setOperation, setOperation.Source2);

static string GetSetOperation(SetOperationBase operation)
=> operation switch
{
ExceptExpression => "EXCEPT",
IntersectExpression => "INTERSECT",
UnionExpression => "UNION",
_ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
};
}

/// <summary>
Expand Down Expand Up @@ -1311,6 +1361,66 @@ 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++)
{
// TODO: Do we want newlines here?
if (i > 1)
{
_relationalCommandBuilder.Append(", ");
}

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

/// <inheritdoc />
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
=> throw new InvalidOperationException(
Expand Down
Loading

0 comments on commit 97190f3

Please sign in to comment.