Skip to content

fix(operators)!: make does-not-have exclude collections with the value - #120

Open
pdevito3 wants to merge 1 commit into
mainfrom
fm/qk-breaking-does-not-have
Open

pdevito3 wants to merge 1 commit into
mainfrom
fm/qk-breaking-does-not-have

Conversation

@pdevito3

Copy link
Copy Markdown
Owner

Summary

This PR makes does-not-have (!^$) exclude collections that have the value. This is a breaking change. It is one of the six breaking changes that were removed from #114 so that main stays compatible with v1.14.2.

Status: for later consideration. Do not merge this PR now. The captain decides on each breaking change separately. If this change is accepted, it needs a major version, or a new operator.

Old behavior (v1.14.2 and main)

Tags !^$ "sweet" builds x.Tags.Any(z => z != "sweet").

  • In memory, this matches every collection that has any other value. It matches ["sweet", "sour"], and it does not match an empty collection.
  • On Postgres, EF cannot translate the expression and throws InvalidOperationException.

New behavior

The operator builds the negation of has: Not(x.Tags.Any(z => z == "sweet")). It returns only collections without the value, including empty collections. %!^$ is now the negation of %^$. The same query now runs on Postgres.

Example

Tags !^$ "sweet"

Proof from the verify-querykit harness. The tags are: Pancakes ["breakfast", "sweet"], Beef Stew ["dinner"], Salt Bread ["bread", "Winner"], Plain Water [].

Target main This PR
memory Pancakes, Beef Stew, Salt Bread (Pancakes has the value, and Plain Water is missing) Beef Stew, Salt Bread, Plain Water
Postgres InvalidOperationException (EF cannot translate the expression) Beef Stew, Salt Bread, Plain Water

Justification

"Does not have X" means that X is not in the collection. The v1.14.2 expression means "has a value that is not X", which is a different question. It returns rows that have X and leaves out empty collections, so every in-memory consumer gets wrong results. It also does not run on Postgres at all.

Every other negated operator in QueryKit is the negation of its positive operator, and the fix makes !^$ obey the same rule. A consumer that wants the old meaning can use a custom operation. Because the in-memory results change, this fix needs a major version or a new operator.

Migration

Use a custom operation to get the old meaning ("has a value that is not X").

Tests

  • FilterParserTests now expects Not(x.Tags.Any(z => (z == "winner"))) and the case-insensitive form.
  • In both regression test files, has_returns_matching_rows is now has_and_does_not_have_return_matching_rows, with 3 new cases for !^$, !^$*, and %!^$.
  • dotnet test: 349 unit tests and 271 integration tests pass.

`Tags !^$ "sweet"` built `Tags.Any(z => z != "sweet")`. This matched every collection with any other value, so it returned rows that have the value and left out empty collections. On Postgres, EF could not translate it and threw InvalidOperationException.

The operator now builds the negation of has: `!Tags.Any(z => z == "sweet")`. This is the same pattern as the other negated collection operators.

BREAKING CHANGE: `!^$` returns only collections without the value, including empty collections. It no longer returns a collection that has the value and another value. `%!^$` is now the negation of `%^$`. Use a custom operation to get the old meaning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant