Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
66b743b
Migrate integration tests to in-process hosts via `WebApplicationFact…
Copilot May 17, 2026
1506a22
Enforce input/output kind safety for `ListOf`/`Nullable` wrappers at …
Copilot May 17, 2026
075018b
Pin CI workflows and build script to .NET SDK 10.0.300 (#578)
Copilot May 17, 2026
19d11c3
Pin CI workflows and build script to .NET SDK 10.0.301
xperiandri Jun 13, 2026
cc5fc0f
Fixed Repo Assist incorrectly encoded characters
xperiandri Jun 13, 2026
50b4569
Bump lodash and babel-cli in /samples/graphiql-client (#584)
dependabot[bot] Jun 13, 2026
d8aba70
Publish CI test results into PR discussions, harden artifact download…
Copilot Jun 13, 2026
45046d7
Fixed NuGet packaging of DesignTime DLL for F# provider
xperiandri Jun 13, 2026
e643b8d
Updated `FSharp.TypeProviders.SDK` to `8.10.0`
xperiandri Jun 13, 2026
bdd7297
fixup! Updated `FSharp.TypeProviders.SDK` to `8.10.0`
xperiandri Jun 14, 2026
15df50a
High severity vulnerability fix: upgrade `SQLitePCLRaw.lib.e_sqlite3`…
Copilot Jul 8, 2026
c1e8510
Support case-insensitive comparison with `ObjectListFilter` (#582)
Copilot Jul 9, 2026
2d94c67
`ObjectListFilter` filter values to target type coercion (#589)
xperiandri Jul 9, 2026
454a9e0
Migrated to modern `InternalsVisibleTo` syntax
xperiandri Jul 16, 2026
8e34237
Implemented support of `Guid` and value object scalars as `InputValue`
xperiandri Jul 17, 2026
78d21c1
Implemented tests for `ObjectListFilter` with empty arrays
xperiandri Aug 9, 2026
060fd26
Updated SDK to `10.0.302`
xperiandri Aug 9, 2026
d971701
Updated `Fantomas` to `7.0.5`
xperiandri Aug 9, 2026
eada5c2
Updated `FSDocs-Tools` to `22.1.0`
xperiandri Aug 9, 2026
b9ce3ad
Implemented robust interface field covariance validation & tests
xperiandri Aug 9, 2026
be74f88
Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /samples/client-provider…
dependabot[bot] Aug 9, 2026
b8464b6
Bump webpack-dev-server from 1.16.5 to 5.2.6 in /samples/graphiql-client
dependabot[bot] Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implemented tests for ObjectListFilter with empty arrays
* Added `ObjectListFilterEmptyArrayTests.fs` with cases for Contains, Equals, Not Equals, and logical operators on empty/non-empty lists. Updated `.fsproj` to include the new file.
* Extended `ObjectListFilterLinqGenerateTests.fs` to verify correct Cosmos SQL generation for list equality and length checks.
  • Loading branch information
xperiandri committed Aug 9, 2026
commit 78d21c1c3524a517ffe240849de84222067de15c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<Compile Include="ObjectListFilter\TypeCoercionFilterTests.fs" />
<Compile Include="ObjectListFilter\TypeCoercionFilterInOperatorTests.fs" />
<Compile Include="ObjectListFilter\TypeCoercionFilterOptionCollectionTests.fs" />
<Compile Include="ObjectListFilter\ObjectListFilterEmptyArrayTests.fs" />
<Compile Include="DeferredTests.fs" />
<Compile Include="SubscriptionTests.fs" />
<Compile Include="MiddlewareTests.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.Linq)>]
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
module FSharp.Data.GraphQL.Tests.ObjectListFilter.EmptyArray.Tests

open Xunit
open System
open System.Linq
open System.Text.Json
open FSharp.Data.GraphQL.Shared
open FSharp.Data.GraphQL.Server.Middleware

// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Test types
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────

type EmptyArrayEntity = {
Id: int
Name: string
Tags: string list
}

// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Test data
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────

let testData = [|
{ Id = 1; Name = "Alice"; Tags = [] }
{ Id = 2; Name = "Bob"; Tags = [ "admin"; "user" ] }
{ Id = 3; Name = "Charlie"; Tags = [ "user" ] }
{ Id = 4; Name = "Diana"; Tags = [] }
|]

let filterOptions = ObjectListFilterLinqOptions<EmptyArrayEntity, obj> (Json.getSerializerOptions Seq.empty)

let applyFilter (filter : ObjectListFilter) =
filter.ApplyTo (testData.AsQueryable (), filterOptions) |> Seq.toList

// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════
// Validation Tests – Check that empty filters do NOT crash and handle correctly
// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════

[<Fact>]
let ``Contains on empty array field returns entities with empty tags`` () =
// Contains on an empty list should match entities with empty lists
let filter = Contains ({ FieldName = "tags"; Value = "admin" }, null)
let result = applyFilter filter
// Only Bob and Charlie have "admin" or any tags
result |> List.length |> equals 1
result |> List.map (fun e -> e.Name) |> equals [ "Bob" ]

[<Fact>]
let ``Empty filter list returns all entities`` () =
// No filter applied means all entities pass
// The built-in queryable should return all
let result = testData.AsQueryable () |> Seq.toList
result |> List.length |> equals 4

[<Fact>]
let ``And with empty field matches correctly`` () =
// Filter: (id > 1 AND tags contains "admin")
let filter =
And (
GreaterThan { FieldName = "id"; Value = 1 },
Contains ({ FieldName = "tags"; Value = "admin" }, null)
)
let result = applyFilter filter
// Only Bob (id=2) has "admin" tag and id > 1
result |> List.length |> equals 1
(List.head result).Name |> equals "Bob"

[<Fact>]
let ``Or with empty field returns union of results`` () =
// Filter: (id = 1 OR tags contains "admin")
let filter =
Or (
Equals ({ FieldName = "id"; Value = 1 }, null),
Contains ({ FieldName = "tags"; Value = "admin" }, null)
)
let result = applyFilter filter
// Alice (id=1) and Bob (has "admin")
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ]

[<Fact>]
let ``Not on empty array field returns complementary set`` () =
// Filter: NOT (tags contains "user")
let filter = Not (Contains ({ FieldName = "tags"; Value = "user" }, null))
let result = applyFilter filter
// Alice, Diana have empty tags; Bob and Charlie have "user"
// so NOT "user" = Alice, Diana
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ]

// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════
// SQL Generation Tests – Check that LINQ expression tree is correctly built
// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════

[<Fact>]
let ``Contains filter on array field generates valid LINQ expression`` () =
// Verify the filter doesn't crash and produces a valid LINQ provider execution
// by actually running it
let filter = Contains ({ FieldName = "tags"; Value = "user" }, null)
let result = applyFilter filter
// Bob and Charlie have "user" tag
result |> List.length |> equals 2

[<Fact>]
let ``Complex nested filter generates valid LINQ expression`` () =
// Nested: (NOT (id < 3)) AND (tags contains "user")
let filter =
And (
Not (LessThan { FieldName = "id"; Value = 3 }),
Contains ({ FieldName = "tags"; Value = "user" }, null)
)
let result = applyFilter filter
// id >= 3: Charlie (3), Diana (4)
// tags contains "user": Bob (2), Charlie (3)
// intersection: Charlie (3)
result |> List.length |> equals 1
(List.head result).Name |> equals "Charlie"

[<Fact>]
let ``Equals empty list filter generates valid LINQ expression`` () =
// Verify: tags = []
// Should correctly compile LINQ expression for list equality check
let emptyListValue = [] : string list
let filter = Equals ({ FieldName = "tags"; Value = emptyListValue }, null)
let result = applyFilter filter
// Alice and Diana have empty tags
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ]

[<Fact>]
let ``Not Equals empty list filter generates valid LINQ expression`` () =
// Verify: NOT (tags = []) => tags != []
// Should correctly compile LINQ expression for list inequality check
let emptyListValue = [] : string list
let filter = Not (Equals ({ FieldName = "tags"; Value = emptyListValue }, null))
let result = applyFilter filter
// Bob and Charlie have non-empty tags
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ]

[<Fact>]
let ``Complex Equals empty list with logical operators generates valid LINQ expression`` () =
// Verify: (id <= 2) AND (tags = [])
let emptyListValue = [] : string list
let filter =
And (
LessThanOrEqual { FieldName = "id"; Value = 2 },
Equals ({ FieldName = "tags"; Value = emptyListValue }, null)
)
let result = applyFilter filter
// id <= 2: Alice (1), Bob (2)
// tags = []: Alice (1), Diana (4)
// intersection: Alice (1)
result |> List.length |> equals 1
(List.head result).Name |> equals "Alice"

// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════
// Coercion Tests – Check that string values are correctly coerced to list membership checks
// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════

[<Fact>]
let ``Contains coerces string value into list element check`` () =
// "admin" (string) should match list containing "admin"
let filter = Contains ({ FieldName = "tags"; Value = "admin" }, null)
let result = applyFilter filter
result |> List.length |> equals 1
result |> List.map (fun e -> e.Name) |> equals [ "Bob" ]

[<Fact>]
let ``Contains returns empty result when no match`` () =
// Looking for a tag that doesn't exist in any entity
let filter = Contains ({ FieldName = "tags"; Value = "superadmin" }, null)
let result = applyFilter filter
result |> List.length |> equals 0

[<Fact>]
let ``Contains with multiple identical tags matches correctly`` () =
// If an entity had ["user"; "user"], Contains "user" should still match
// (list element membership check, not count)
let filter = Contains ({ FieldName = "tags"; Value = "user" }, null)
let result = applyFilter filter
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ]

[<Fact>]
let ``Equals on list field with empty list returns only empty lists`` () =
// Filter: tags = []
let emptyListValue = [] : string list
let filter = Equals ({ FieldName = "tags"; Value = emptyListValue }, null)
let result = applyFilter filter
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ]

[<Fact>]
let ``Not Equals on empty list returns only non-empty lists`` () =
// Filter: NOT (tags = []) => tags != [] => only entities with non-empty tags
let emptyListValue = [] : string list
let filter = Not (Equals ({ FieldName = "tags"; Value = emptyListValue }, null))
let result = applyFilter filter
result |> List.length |> equals 2
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ]

[<Fact>]
let ``Implicit non-empty check via Contains finds all entities with any tag`` () =
// Any entity where Contains matches ANY tag is considered "has tags"
// This is implicit non-empty: if Contains("user") or Contains("admin") matches, the list is non-empty
let filter1 = Contains ({ FieldName = "tags"; Value = "user" }, null)
let filter2 = Contains ({ FieldName = "tags"; Value = "admin" }, null)
let result1 = applyFilter filter1 // "user": Bob, Charlie
let result2 = applyFilter filter2 // "admin": Bob
let combined =
(result1 |> List.map (fun e -> e.Id))
@ (result2 |> List.map (fun e -> e.Id))
|> List.distinct
|> List.sort
// Bob (2) and Charlie (3) have at least one tag
combined |> equals [ 2; 3 ]
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,51 @@ let ``ObjectListFilter works with In operator for empty ValidStringStruct list``
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE false"""

[<Fact>]
let ``ObjectListFilter works with Equals operator for empty ValidStringStructList`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Equals ({ FieldName = "validStringStructList"; Value = [] }, null)
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
// Equals empty list should use ARRAY_LENGTH = 0 or NOT ANY
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (ARRAY_LENGTH(root["validStringStructList"]) = 0)"""

[<Fact>]
let ``ObjectListFilter works with Not Equals operator for empty ValidStringStructList`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Not (Equals ({ FieldName = "validStringStructList"; Value = [] }, null))
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
// Not Equals empty list should use ARRAY_LENGTH > 0
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (ARRAY_LENGTH(root["validStringStructList"]) > 0)"""

[<Fact>]
let ``ObjectListFilter works with Equals operator for non-empty ValidStringStructList`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Equals ({ FieldName = "validStringStructList"; Value = ["tag1"; "tag2"] }, null)
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
// Equals non-empty list should check exact list match
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] = ["tag1", "tag2"])"""

[<Fact>]
let ``ObjectListFilter works with Not Equals operator for non-empty ValidStringStructList`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Not (Equals ({ FieldName = "validStringStructList"; Value = ["tag1"; "tag2"] }, null))
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
// Not Equals non-empty list should check NOT exact list match
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] != ["tag1", "tag2"])"""

[<Fact>]
let ``ObjectListFilter works with Equals operator for single-element ValidStringStructList`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Equals ({ FieldName = "validStringStructList"; Value = ["tag1"] }, null)
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
// Equals single-element list
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] = ["tag1"])"""

[<Fact>]
let ``ObjectListFilter works with Equals operator for ValidStringObject`` () =
let filter = Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null)
Expand Down