Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support primitive collections #30738

Merged
merged 2 commits into from
Apr 26, 2023
Merged

Support primitive collections #30738

merged 2 commits into from
Apr 26, 2023

Conversation

roji
Copy link
Member

@roji roji commented Apr 20, 2023

Implements the bulk of #30731

Closes #29427
Closes #30426
Closes #13617
Closes #30163

@roji roji requested a review from a team April 20, 2023 18:38
@roji roji changed the title Implement SQL Server compatibility level Support primitive collections Apr 20, 2023

// 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajcvickers FYI, this is #30677

/// </summary>
/// <param name="selectExpression">The <see cref="SelectExpression" /> to check for ordering.</param>
/// <returns>Whether <paramref name="selectExpression"/> is ordered.</returns>
protected virtual bool IsOrdered(SelectExpression selectExpression)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this for PostgreSQL, where the results of unnest are guaranteed to be ordered without any explicit ORDER BY; this allows us to avoid the warning for Skip/Take over unordered collection.

@@ -1913,13 +1945,4 @@ public Expression Convert(Type type)
: new EntityReferenceExpression(this, derivedEntityType);
}
}

private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this verification - that all SqlExpressions have a mapping - has been moved from the end of each individual SQL translations (of which there are potentially several in the query) to the end of the entire query. This is necessary since some mappings are inferred later.

/// not used in application code.
/// </para>
/// </summary>
public class RowValueExpression : SqlExpression
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also provide infrastructure for other uses of row values, e.g. #26822

public virtual IStoreFunction? StoreFunction { get; }

/// <inheritdoc />
ITableBase? ITableBasedExpression.Table
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this PR, TableValuedFunctionExpression could only be used with a store function from the model; this changes it to be usable for any TVF, without any necessary model data on it (e.g. OpenJson).

// 3. The element CLR type has a supported type mapping which isn't itself a collection (nested collections not yet supported).

// Note that e.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow.
if (mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is not { } elementClrType
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajcvickers @AndriySvyryd this is another place where we need some type mapping infra (#30730) - at least some parts of this function should probably be in core/relational; we should also consider having providers just support collections via JSON arrays by default, without them needing to actually do anything (but allow Npgsql to override).

Note also the annoying construction of the collection type mapping below, with the cloning etc.

/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this specialized translation of LINQ Count() when applied to a primitive collection (JSON string); instead of using json_each to convert to a table and use a SQL scalar subquery to do COUNT(*), we just use a dedicated function for that.

Npgsql has a ton of such specialized pattern matching translations - it's really nice. See roji/efcore.pg@83842cf#diff-710c2323c08f93bcf612ff7124133abf291f46900fcf8f0adc89eaf5709d22e9R119 and below.

jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping,
isColumnNullable: null);

// TODO: SQLite does have REAL and BLOB types, which JSON does not. Need to possibly cast to that.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// The "standard" JSON timestamp representation is ISO8601, with a T between date and time; but SQLite's representation has
// no T. Apply a conversion on the value coming out of json_each.
SqliteDateTimeTypeMapping => _sqlExpressionFactory.Function(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be replaced by #30727

/// <summary>
/// A value converter that converts a .NET primitive collection into a JSON string.
/// </summary>
// TODO: This currently just calls JsonSerialize.Serialize/Deserialize. It should go through the element type mapping's APIs for
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we'd use the JSON serialization/deserialization APIs on the type mapping, which we'd introduce in #30677.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll discuss this in detail in the PR for #30677.

@roji roji force-pushed the PrimitiveCollections branch 4 times, most recently from 527ca05 to 642e5d7 Compare April 21, 2023 08:14
// So we clone the expression to avoid any side-effects, but that also prevents us from directly comparing columns (since
// the ValuesExpression has been cloned too).
// We should simply be able to pattern-match directly on the projection mappings (currently private) without cloning/applying
var clonedSelect = selectExpression.Clone();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maumar note this change - I think this aligns with how things are supposed to happen in the query pipeline.

Instead of cloning and applying the projection on the SelectExpression in order to check its projections, the right thing seems to be to look at the ShaperExpression instead, and use that the resolve the projection from the selectExpression - without needing to apply projections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply projection has some really complicated logic, for stuff like group by or general subquery projection, so that might not be a generally safe approach. But for simple pattern match like here, it should be perfectly fine

Copy link
Member Author

@roji roji Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Though I did remove cloning and applying the projection even for this simpler case - I now access the projection via the shaper instead (via selectExpression.GetProjection(projectionBindingExpression)). It feels more correct/safer (and even more efficient as we don't clone).

I do think we can probably improve things for this sort of pattern-matching - but we can think about it later.

Copy link
Member

@AndriySvyryd AndriySvyryd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-query parts look good enough.

@maumar
Copy link
Contributor

maumar commented Apr 25, 2023

query stuff looking good as well! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants