Skip to content

fix(query): preserve discriminator predicates in type filters#263

Merged
j-d-ha merged 20 commits into
mainfrom
test/tests-inheritance-query-spec
Jun 25, 2026
Merged

fix(query): preserve discriminator predicates in type filters#263
j-d-ha merged 20 commits into
mainfrom
test/tests-inheritance-query-spec

Conversation

@j-d-ha

@j-d-ha j-d-ha commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Preserves discriminator predicate metadata when translating C# type filters so downstream query generation can handle them correctly. Documents that OfType<T>(), is, and GetType() type filtering require active discriminator metadata when shared-table discrimination has been disabled.

Changes

  • Wrap translated is predicates and GetType() equality predicates in SqlDiscriminatorPredicateExpression.
  • Preserve negation behavior for GetType() != typeof(...) through the wrapped discriminator predicate.
  • Clarify shared-table discriminator limits in docs/limitations.md and docs/modeling/single-table-design.md.

Validation

  • dotnet build

Release Notes

  • Type filters over shared-table inheritance now require active discriminator metadata; models using HasNoDiscriminator() should use key predicates or concrete DbSet queries when keys already isolate types.

j-d-ha added 6 commits June 24, 2026 09:50
- Enhanced inheritance query tests with SQL assertion checks.
- Verified generated SQL for TPH discriminator and derived type filtering scenarios.
- Improved test reliability by validating expected SQL queries during execution.

docs(query): clarify limitations in type filtering

- Documented support for `GetType` with exact concrete entity types.
- Updated guidance on using `is` and `OfType<T>` for derived type filtering.

refactor(query): handle missing discriminator predicates gracefully

- Added checks for missing discriminator predicates in `OfType<TDerived>` queries.
- Returned unsupported operator messages when discriminator filtering is required but unavailable.
- Ensured consistent behavior across all type hierarchy queries.
…icate validation

- Added SQL assertion checks for `OfType<T>` inheritance query tests.
- Improved handling of missing discriminator predicates for filtered type hierarchies.
- Refactored `CreateFalsePredicate` for better code reusability in query translation.
- Updated documentation to clarify concrete type filtering limits and guidance for derived types.
@github-actions github-actions Bot added the type: fix Bug fix label Jun 24, 2026
j-d-ha and others added 13 commits June 24, 2026 18:54
- Extended `NoSyncTest` to include an `expectSyncFailure` flag for more flexible test scenarios.
- Updated tests to use the new parameter, allowing explicit control over sync failure expectations.
- Revised `DynamoTestHelpers` to conditionally assert sync failures based on the new flag.
- Improved code readability by consistently applying the optional parameter across test cases.
- Included `Microsoft.EntityFrameworkCore.Query.Inheritance` namespace conditionally for .NET 11+.
- Ensured compatibility with new APIs introduced in recent .NET versions.
…d 11 builds

- Added support for named EF Core 10 (`Debug EF10`, `Release EF10`) and EF Core 11 (`Debug EF11`, `Release EF11`) build configurations.
- Introduced specific commands for building, testing, and packing EF Core 10 and 11 using `task`.
- Enhanced flexibility with options to install CI-matching .NET 11 SDK for consistent local builds.
- Updated documentation with detailed commands and usage examples for managing EF Core versions.
…nces

- Added `NU1510` to `NoWarn` in the project file to handle incorrect package pruning warnings.
- Documented the need for retaining `.NET 11` package references to prevent compilation errors.
- Moved `NU1510` suppression to individual package references instead of a global `NoWarn` property.
- Updated comments to explain package pruning behavior and necessity of retaining references.
- Ensured clarity regarding required public API types for .NET 11 compatibility.
- Added detection for Enumerable/Queryable Any() calls in primitive collections.
- Implemented SQL size-based translation for Any() without predicates.
- Introduced method `TranslateCollectionAny` for handling Any() expressions.
- Added test case for Any() on primitive collections in subtype queries.
@ncipollina

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 019f3f7809

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ncipollina ncipollina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: fix(query) — preserve discriminator predicates in type filters

This is a substantial, well-structured addition. The core translation work (OfType, is, GetType()) is solid and the spec test coverage is comprehensive. Several correctness and design points to address before merging.

Must Fix

1. Cross-root assignability guard is over-constrained (both visitors)
The double-sided GetRootType() check rejects valid same-hierarchy queries. If sourceEntityType is Animal (root) and targetEntityType is Kiwi (derived), targetEntityType.GetRootType() == Animal, which is assignable from sourceEntityType.GetRootType() == Animal. So the second clause always passes for valid same-hierarchy cases, meaning the only real guard is the first. But the first clause can reject when the source is a non-root base (e.g., Bird navigating to Kiwi) if the roots differ across mapped tables. The two-clause pattern should be sourceRoot == targetRoot; the current form is confusing and has a latent bug for multi-table mixed hierarchies.

2. VisitTypeBinary ignores TypeAs / other node types silently
ExpressionType.TypeAs is a valid TypeBinaryExpression node type (C# as operator). The current implementation returns NotTranslatedExpression for it, which is correct, but the node.NodeType == ExpressionType.TypeIs check burns through both cases in one ternary, making it harder to see intent. A named constant or early-return guard would be clearer.

3. TranslateCollectionAnyIsNotMissing is semantically wrong for empty collections
IsNotMissing checks attribute existence, but DynamoDB can store an empty list that is present but has zero elements. A stored [] passes IS NOT MISSING but .Any() should return false. The correct translation would be a size check (e.g., size(attr) > 0) if supported, or fall back to NotTranslatedExpression for now. This is a correctness bug.

4. NoSyncTest(expectSyncFailure: false) — silent success when exception is not thrown
The original NoSyncTest always called Assert.Fail when sync succeeded unexpectedly. The new expectSyncFailure = false path returns silently even when the sync call threw a different exception type. That swallows unexpected failures for the six callers that pass false. The catch clause should remain specific: only catch IsExpectedSyncQueryFailure exceptions, and re-throw anything else regardless of the flag.

Suggestions

5. CreateFalsePredicate()1 = 0 is fragile
A literal 1 = 0 may or may not be valid PartiQL. A dedicated SqlFalseExpression or a known-false constant expression would be safer. At minimum, add a unit test that verifies the false predicate serializes to valid PartiQL.

6. Redundant discriminator predicate in OfType + entity-type root filter
Can_use_of_type_bird_with_projection shows WHERE ("discriminator" = 'Eagle' OR "discriminator" = 'Kiwi') AND ("discriminator" = 'Eagle' OR "discriminator" = 'Kiwi'). The root entity materializer applies its own discriminator predicate, and TranslateOfType appends another. EF Cosmos deduplicates this. The query still executes correctly (it's AND of identical predicates), but it's wasteful and looks like a bug in generated output. Worth a follow-up issue.

7. #if NET11_0_OR_GREATER in ComplianceDynamoTest.cs — guarded using without guarded type reference
InheritanceQueryTestBase<> is yielded unconditionally on EF10 via the EF10 namespace import at the top of the file. The #if NET11_0_OR_GREATER guard on the using is inverted from what InheritanceQueryDynamoTest.cs does (which guards with #if NET10_0 for the old namespace). Double-check that EF10 can resolve InheritanceQueryTestBase<> from Microsoft.EntityFrameworkCore.Query as imported by ComplianceDynamoTest.cs.

Positives

✅ The TryUnwrapGetTypeComparison with bidirectional argument order check (|| TryUnwrap(right, left, ...)) correctly handles both GetType() == typeof(T) and typeof(T) == GetType() forms.

✅ Wrapping discriminator predicates in SqlDiscriminatorPredicateExpression before passing to Not() is clean — it preserves the predicate's identity for downstream inspection.

✅ The exact parameter that selects GetConcreteDerivedTypesInclusive vs. [targetEntityType] is a clear, non-duplicated design for is vs. GetType() semantics.

✅ The OfTypeRequiresDiscriminatorPredicate error string clearly distinguishes the no-discriminator-metadata failure from the general unsupported case.

✅ Promoting InheritanceQueryTestBase from the Future table to Implemented in docs/spec-test-coverage.md with accurate method counts is the right housekeeping.

✅ The expectSyncFailure parameter solves the real problem (tests that succeed on sync path but EF throws for unrelated reasons). The fix direction is right; only the exception re-throw gap needs closing.

@j-d-ha

j-d-ha commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 85af743:\n\n- Replaced double-sided root assignability checks with explicit root equality in both visitors.\n- Made TypeBinary handling use an explicit non-TypeIs early return.\n- Changed primitive collection Any() translation from IS NOT MISSING to size(attr) > 0 and updated EF11 spec baseline.\n- Fixed NoSyncTest so expected sync-query failures are only swallowed when expectSyncFailure is true.\n- Simplified OfType identity/base helper.\n- Opened follow-up #264 for discriminator predicate deduplication.\n\nValidation:\n- task test:spec CONFIG="Debug EF10"\n- task test:spec CONFIG="Debug EF11"

@j-d-ha j-d-ha requested a review from ncipollina June 25, 2026 20:01

@ncipollina ncipollina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All three must-fix items from the previous review have been correctly addressed:

  • TranslateCollectionAny now uses size(attr) > 0 — semantically correct for primitive collection .Any()
  • NoSyncTest re-throws when expectSyncFailure == false — no more silent swallowing
  • ✅ Double-sided IsAssignableFrom replaced with clean != equality check

Happy to approve. 🎉

@j-d-ha j-d-ha merged commit 95098b5 into main Jun 25, 2026
7 checks passed
@j-d-ha j-d-ha deleted the test/tests-inheritance-query-spec branch June 25, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants