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

All and Any methods don't work correctly with null values #93

Open
samcook opened this issue Jun 19, 2019 · 8 comments
Open

All and Any methods don't work correctly with null values #93

samcook opened this issue Jun 19, 2019 · 8 comments

Comments

@samcook
Copy link
Collaborator

samcook commented Jun 19, 2019

If you have a DataAccessObject with a property that can contain nulls (e.g. string, nullable int, nullable bool, etc...), performing an All(x => ...) operation can give incorrect results, as it appears to generate SQL that ignores rows with null values in the field.

e.g.

[DataAccessObject]
public class DbObject : DataAccessObject<int>
{
    [PersistedMember]
    public virtual string Foo { get; set; }
}
Id Foo
1 hello
2 NULL
3 NULL
this.Model.DbObjects.All(x => x.Foo == "hello").ShouldBe(false);
// actually returns true

this.Model.DbObjects.Where(x => x.Foo == null).All(x => x.Foo == "anything").ShouldBe(false);
// actually returns true

The SQL generated for the first example looks something like:

SELECT
    (
        SELECT CASE
                   WHEN (NOT ((EXISTS
                               (
                                   SELECT "T0"."DbObjectId",
                                          "T0"."Foo"
                                   FROM "DbObject" AS "T0"
                                   WHERE NOT ((("T0"."Foo") = ('hello')))
                               )
                              )
                             )
                        ) THEN
            (1)
                   ELSE
            (0)
               END AS "EXISTS_COL"
    ) AS "value";

I think the problem is that the WHERE clause in the inner query is causing the rows with NULLs to be omitted.

TLDR:

  • If there are any nulls matching the All predicate, they seem to be ignored and the result is based on the non null values.
  • If there are only nulls matching the All predicate, the result is always true.
@samcook
Copy link
Collaborator Author

samcook commented Jun 20, 2019

Also affects Any with !=

this.Model.DbObjects.Any(x => x.Foo != "hello").ShouldBe(true);
// actually returns false
SELECT
    (
        SELECT CASE
                   WHEN ((EXISTS
                          (
                              SELECT "T0"."DbObjectId",
                                     "T0"."Foo"
                              FROM "DbObject" AS "T0"
                              WHERE (("T0"."Foo") <> ('hello'))
                          )
                         )
                        ) THEN
            (1)
                   ELSE
            (0)
               END AS "EXISTS_COL"
    ) AS "value";

@samcook
Copy link
Collaborator Author

samcook commented Jun 20, 2019

For reference, this is what EntityFramework 6 generates:

db.DbObjects.All(x => x.Foo == "hello"); // returns false
SELECT CASE
           WHEN (NOT EXISTS
                     (
                         SELECT 1 AS [C1]
                         FROM [dbo].[DbObjects] AS [Extent1]
                         WHERE (NOT (
                                        (N'hello' = [Extent1].[Foo])
                                        AND ([Extent1].[Foo] IS NOT NULL)
                                    )
                               )
                               OR (CASE
                                       WHEN (N'hello' = [Extent1].[Foo]) THEN
                                           CAST(1 AS BIT)
                                       WHEN (NOT (
                                                     (N'hello' = [Extent1].[Foo])
                                                     AND ([Extent1].[Foo] IS NOT NULL)
                                                 )
                                            ) THEN
                                           CAST(0 AS BIT)
                                   END IS NULL
                                  )
                     )
                ) THEN
               CAST(1 AS BIT)
           ELSE
               CAST(0 AS BIT)
       END AS [C1]
FROM
(SELECT 1 AS X) AS [SingleRowTable1];
db.DbObjects.Any(x => x.Foo != "hello"); // returns true
SELECT CASE
           WHEN (EXISTS
                 (
                     SELECT 1 AS [C1]
                     FROM [dbo].[DbObjects] AS [Extent1]
                     WHERE NOT (
                                   (N'hello' = [Extent1].[Foo])
                                   AND ([Extent1].[Foo] IS NOT NULL)
                               )
                 )
                ) THEN
               CAST(1 AS BIT)
           ELSE
               CAST(0 AS BIT)
       END AS [C1]
FROM
(SELECT 1 AS X) AS [SingleRowTable1];

@samcook samcook changed the title All method doesn't work correctly with null values All and Any methods don't work correctly with null values Jun 20, 2019
@tumtumtum
Copy link
Owner

Could you add a unit test?

@samcook
Copy link
Collaborator Author

samcook commented Jun 20, 2019

Will do

@samcook
Copy link
Collaborator Author

samcook commented Jun 20, 2019

Unsurprisingly, the same issue can also apply to the predicates for Where, Count, etc...

@tumtumtum
Copy link
Owner

TLDR but is this is an issue with SQL server only or all providers?

@samcook
Copy link
Collaborator Author

samcook commented Jun 20, 2019

Affects both SQL server and sqlite, haven't tried mysql or postgres

@tumtumtum
Copy link
Owner

Yeah comparing with null is undefined so not(undefined) isn't true.

I have to have a think about where else this affects could affect the framework because LINQ semantics for NULL is different.

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

No branches or pull requests

2 participants