Skip to content
New issue

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

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

Already on GitHub? # to your account

Fix to #31365 - Query: add support for projecting JSON entities that have been composed on #31391

Merged
merged 1 commit into from
Sep 5, 2023

Conversation

maumar
Copy link
Contributor

@maumar maumar commented Aug 1, 2023

Adding new materialization path for JSON entities that have been converted to query roots - they look like normal entity, but materialization is somewhat different.
We need to prune synthesized key properties from materializer code (as they are not associated with any column, but rather made up on the fly)
Also added code to include all the child navigations. For normal entities we have IncludeExpressions in the shaper expression, but for JSON we prune all includes and build them in the materializer.

Fix is to recognize the scenario (entity projection but the entity is mapped to JSON) during apply projection phase and generate all the necessary info needed rather than just dictionary of property to index maps like we do for regular entity.
Then when we build materializer we use that info to prune the synthesized properties from key check and re-use existing include JSON entity code to add child navigations.

Fixes #31365

@maumar maumar requested a review from roji August 1, 2023 08:38
@maumar
Copy link
Contributor Author

maumar commented Aug 1, 2023

@roji, turned out bit easier than I thought (knock on wood) there are still some tests failing due to some bad assumptions in the JSON post processor. Also SelectMany is still broken (will take a look), and it looks like OPENJSON with WITH needs to add AS JSON for the primitve collection properties, otherwise they come out as null (at least in some scenarios). But when I do that, I run into some bad state in the JSON post processor. Also code still needs some major cleanup, as I hacked it rather quickly ^^'

{
AddToProjection(entityProjectionExpression.DiscriminatorExpression, DiscriminatorColumnAlias);
//achievement unlocked ;P
do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

:trollface:

Copy link
Member

Choose a reason for hiding this comment

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

Ahahahaha 👍

Copy link
Member

@roji roji Aug 1, 2023

Choose a reason for hiding this comment

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

I'm 100% sure we'll figure out why this is somehow incorrect and needs to be a regular while

Comment on lines 155 to 159
var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count);
var sourcePrimaryKeyProperties = EntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count);
foreach (var (target, source) in targetPrimaryKeyProperties.Zip(sourcePrimaryKeyProperties, (t, s) => (t, s)))
{
newKeyPropertyMap[target] = _keyPropertyMap[source];
newKeyPropertyMap[target] = KeyPropertyMap[source];
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: If accessing KeyPropertyMap is considered to be somehow expensive due to the virtual call, it can be cached into a local here

Copy link
Contributor Author

@maumar maumar Aug 1, 2023

Choose a reason for hiding this comment

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

I'll leave it for our Resident Optimization Justification Investigator to decide, @roji

Name = jsonPropertyName,
TypeMapping = property.GetRelationalTypeMapping(),
Path = new PathSegment[] { new(jsonPropertyName) },
//AsJson = property.GetRelationalTypeMapping().ElementTypeMapping is not null
Copy link
Contributor

Choose a reason for hiding this comment

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

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when using OPENJSON with WITH we need to mark primitive collections with AS JSON keyword, otherwise they come out as null. When I do it, we hit some bad assumptions in the json post processor, so I commented it temporarily. Will uncomment and fix json post processor in the final version.

Copy link
Member

Choose a reason for hiding this comment

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

Note that I made some minor changes in #31369 to the JSON postprocessor, these could in theory be related (so rebase in any case).

Anyway, let me know if you want my help with this tomorrow.

Copy link
Contributor

@ajcvickers ajcvickers left a comment

Choose a reason for hiding this comment

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

🐑 🇮🇹

@roji
Copy link
Member

roji commented Aug 1, 2023

Ahh, the season of 🐑 🇮🇹 has begun :)

@maumar will also review shortly for the SelectExpression parts.

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

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

My understanding here is pretty vague... But looks OK :)

Let's think about cleanup later when everything is calmer.

@@ -1690,6 +1682,33 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express
projectionIndexMap[jsonProjectionInfo.JsonColumnIndex],
newKeyAccessInfo));
}
else if (constantValue is QueryableJsonProjectionInfo queryableJsonProjectionInfo)
Copy link
Member

Choose a reason for hiding this comment

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

This whole thing can be a switch 😇

@roji
Copy link
Member

roji commented Aug 1, 2023

@maumar FYI #31369 has been merged, this can now be rebased over main.

@maumar
Copy link
Contributor Author

maumar commented Aug 2, 2023

rebased and updated, please take a final look @roji @ajcvickers


protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (methodCallExpression is
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this should be good enough - just replace reference to the ordinal key with default value for the type we expect. In case of key comparison (handled above) we make an exception because we need to convert key != null into true, rather than null != null which would have been false. In general, since this materialization path is always for non-tracking, we shouldn't have too many instances of the keys present, one other place that I haven't yet tested would be materialization interceptor. @ajcvickers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

update: added test for materialization interceptor - works like a charm

@@ -789,7 +789,9 @@ public void ApplyDistinct()
// for JSON entities identifier is the key that was generated when we convert from json to query root (OPENJSON, json_each, etc)
// but we can't use it for distinct, as it would warp the results
// instead, we will treat every non-key property as identifier
foreach (var property in entityProjection.EntityType.GetDeclaredProperties().Where(p => !p.IsPrimaryKey()))

// TODO: hack/workaround, see #31398
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI

@@ -239,8 +239,10 @@ static bool IsKeyColumn(SqlExpression sqlExpression, TableExpressionBase table)
columnExpression.Table, "value", columnExpression.Type, _nvarcharMaxTypeMapping, columnExpression.IsNullable);

Check.DebugAssert(columnInfo.Path is not null, "Path shouldn't be null in OPENJSON WITH");
Check.DebugAssert(!columnInfo.AsJson, "AS JSON signifies an owned sub-entity being projected out of OPENJSON/WITH. "
+ "Columns referring to that must be wrapped be Json{Scalar,Query}Expression and will have been already dealt with above");
//Check.DebugAssert(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@roji uncomment to see a bunch of tests failing due to incorrect assumptions we had in the post processor.

@maumar maumar force-pushed the fix31365_take2 branch 2 times, most recently from 443c468 to d7fd111 Compare August 2, 2023 10:28
@maumar maumar changed the base branch from main to release/8.0 September 5, 2023 21:22
…have been composed on

Adding new materialization path for JSON entities that have been converted to query roots - they look like normal entity, but materialization is somewhat different.
We need to prune synthesized key properties from materializer code (as they are not associated with any column, but rather made up on the fly)
Also added code to include all the child navigations. For normal entities we have IncludeExpressions in the shaper expression, but for JSON we prune all includes and build them in the materializer.

Fix is to recognize the scenario (entity projection but the entity is mapped to JSON) during apply projection phase and generate all the necessary info needed rather than just dictionary of property to index maps like we do for regular entity.
Then when we build materializer we use that info to prune the synthesized properties from key check and re-use existing include JSON entity code to add child navigations.

Fixes #31365
@maumar maumar merged commit 9d217c7 into release/8.0 Sep 5, 2023
@maumar maumar deleted the fix31365_take2 branch September 5, 2023 23:42
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Query: add support for projecting JSON entities that have been composed on
4 participants