From 1c5b8042de30a2653bdbfbb953936f9d5a1b308f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 16 Sep 2024 21:49:09 -0400 Subject: [PATCH] Add tests for InArray comparison --- src/Common/Library.fs | 18 ++++++---- src/Tests.CSharp/CommonCSharpTests.cs | 11 ++++++- src/Tests.CSharp/PostgresCSharpTests.cs | 40 ++++++++++++++++++---- src/Tests.CSharp/SqliteCSharpTests.cs | 32 ++++++++++++++++++ src/Tests.CSharp/Types.cs | 16 +++++++++ src/Tests/CommonTests.fs | 9 ++++- src/Tests/PostgresTests.fs | 44 ++++++++++++++++++++----- src/Tests/SqliteTests.fs | 40 ++++++++++++++++++---- src/Tests/Types.fs | 26 +++++++++++---- 9 files changed, 197 insertions(+), 39 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 735b033..6853457 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -38,7 +38,7 @@ type Comparison = | NotEqual _ -> "<>" | Between _ -> "BETWEEN" | In _ -> "IN" - | InArray _ -> "|?" // PostgreSQL only; SQL needs a subquery for this + | InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this | Exists -> "IS NOT NULL" | NotExists -> "IS NULL" @@ -120,20 +120,24 @@ with /// Create a not equals (<>) field criterion (alias) static member NE name (value: obj) = Field.NotEqual name value - /// Create a BETWEEN field criterion + /// Create a Between field criterion static member Between name (min: obj) (max: obj) = Field.Where name (Between(min, max)) - /// Create a BETWEEN field criterion (alias) + /// Create a Between field criterion (alias) static member BT name (min: obj) (max: obj) = Field.Between name min max - /// Create an IN field criterion + /// Create an In field criterion static member In name (values: obj seq) = Field.Where name (In values) - - /// Create an IN field criterion (alias) + + /// Create an In field criterion (alias) static member IN name (values: obj seq) = Field.In name values - + + /// Create an InArray field criterion + static member InArray name tableName (values: obj seq) = + Field.Where name (InArray(tableName, values)) + /// Create an exists (IS NOT NULL) field criterion static member Exists name = Field.Where name Exists diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index ecfffa6..911b732 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -59,7 +59,7 @@ public static class CommonCSharpTests }), TestCase("InArray succeeds", () => { - Expect.equal(Comparison.NewInArray("", []).OpSql, "|?", "The InArray SQL was not correct"); + Expect.equal(Comparison.NewInArray("", []).OpSql, "?|", "The InArray SQL was not correct"); }), TestCase("Exists succeeds", () => { @@ -125,6 +125,15 @@ public static class CommonCSharpTests Expect.isTrue(field.Comparison.IsIn, "Comparison incorrect"); Expect.sequenceEqual(((Comparison.In)field.Comparison).Values, [8, 16, 32], "Value incorrect"); }), + TestCase("InArray succeeds", () => + { + var field = Field.InArray("ArrayField", "table", ["x", "y", "z"]); + Expect.equal(field.Name, "ArrayField", "Field name incorrect"); + Expect.isTrue(field.Comparison.IsInArray, "Comparison incorrect"); + var it = (Comparison.InArray)field.Comparison; + Expect.equal(it.Table, "table", "Table name incorrect"); + Expect.sequenceEqual(it.Values, ["x", "y", "z"], "Value incorrect"); + }), TestCase("Exists succeeds", () => { var field = Field.Exists("Groovy"); diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 570fdfd..3a95a2c 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -184,40 +184,40 @@ public static class PostgresCSharpTests [ TestList("WhereByFields", [ - TestCase("succeeds for a single field when a logical operator is passed", () => + TestCase("succeeds for a single field when a logical comparison is passed", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.Any, [Field.Greater("theField", "0").WithParameterName("@test")]), "data->>'theField' > @test", "WHERE clause not correct"); }), - TestCase("succeeds for a single field when an existence operator is passed", () => + TestCase("succeeds for a single field when an existence comparison is passed", () => { Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NotExists("thatField")]), "data->>'thatField' IS NULL", "WHERE clause not correct"); }), - TestCase("succeeds for a single field when a between operator is passed with numeric values", () => + TestCase("succeeds for a single field when a between comparison is passed with numeric values", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.All, [Field.Between("aField", 50, 99).WithParameterName("@range")]), "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); }), - TestCase("succeeds for a single field when a between operator is passed with non-numeric values", () => + TestCase("succeeds for a single field when a between comparison is passed with non-numeric values", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.Any, [Field.Between("field0", "a", "b").WithParameterName("@alpha")]), "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); }), - TestCase("succeeds for all multiple fields with logical operators", () => + TestCase("succeeds for all multiple fields with logical comparisons", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.All, [Field.Equal("theFirst", "1"), Field.Equal("numberTwo", "2")]), "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); }), - TestCase("succeeds for any multiple fields with an existence operator", () => + TestCase("succeeds for any multiple fields with an existence comparison", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.Any, @@ -225,13 +225,19 @@ public static class PostgresCSharpTests "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0", "WHERE clause not correct"); }), - TestCase("succeeds for all multiple fields with between operators", () => + TestCase("succeeds for all multiple fields with between comparisons", () => { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.All, [Field.Between("aField", 50, 99), Field.Between("anotherField", "a", "b")]), "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", "WHERE clause not correct"); + }), + TestCase("succeeds for a field with an InArray comparison", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, [Field.InArray("theField", "the_table", ["q", "r"])]), + "data->'theField' ?| @field0", "WHERE clause not correct"); }) ]), TestList("WhereById", @@ -890,6 +896,26 @@ public static class PostgresCSharpTests var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); + }), + TestCase("succeeds for InArray when matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await Definition.EnsureTable(PostgresDb.TableName); + foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(PostgresDb.TableName, doc); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.All, + [Field.InArray("Values", PostgresDb.TableName, ["c"])]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + }), + TestCase("succeeds for InArray when no matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await Definition.EnsureTable(PostgresDb.TableName); + foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(PostgresDb.TableName, doc); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.All, + [Field.InArray("Values", PostgresDb.TableName, ["j"])]); + Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), TestList("ByFieldsOrdered", diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 731a512..a9cce70 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -59,6 +59,18 @@ public static class SqliteCSharpTests [Field.Between("aField", 50, 99), Field.Between("anotherField", "a", "b")]), "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", "WHERE clause not correct"); + }), + TestCase("succeeds for a field with an In comparison", () => + { + Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.All, [Field.In("this", ["a", "b", "c"])]), + "data->>'this' IN (@field0_0, @field0_1, @field0_2)", "WHERE clause not correct"); + }), + TestCase("succeeds for a field with an InArray comparison", () => + { + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, [Field.InArray("this", "the_table", ["a", "b"])]), + "EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (@field0_0, @field0_1))", + "WHERE clause not correct"); }) ]), TestCase("WhereById succeeds", () => @@ -619,6 +631,26 @@ public static class SqliteCSharpTests var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); + }), + TestCase("succeeds for InArray when matching documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Definition.EnsureTable(SqliteDb.TableName); + foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.All, + [Field.InArray("Values", SqliteDb.TableName, ["c"])]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + }), + TestCase("succeeds for InArray when no matching documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Definition.EnsureTable(SqliteDb.TableName); + foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.All, + [Field.InArray("Values", SqliteDb.TableName, ["j"])]); + Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), TestList("ByFieldsOrdered", diff --git a/src/Tests.CSharp/Types.cs b/src/Tests.CSharp/Types.cs index 88d8830..3acb0d0 100644 --- a/src/Tests.CSharp/Types.cs +++ b/src/Tests.CSharp/Types.cs @@ -31,3 +31,19 @@ public class JsonDocument new() { Id = "five", Value = "purple", NumValue = 18 } ]; } + +public class ArrayDocument +{ + public string Id { get; set; } = ""; + public string[] Values { get; set; } = []; + + /// + /// A set of documents used for integration tests + /// + public static readonly List TestDocuments = + [ + new() { Id = "first", Values = ["a", "b", "c"] }, + new() { Id = "second", Values = ["c", "d", "e"] }, + new() { Id = "third", Values = ["x", "y", "z"] } + ]; +} diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 00a738d..95b4f2d 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -33,7 +33,7 @@ let comparisonTests = testList "Comparison.OpSql" [ Expect.equal (In []).OpSql "IN" "The In SQL was not correct" } test "InArray succeeds" { - Expect.equal (InArray("", [])).OpSql "|?" "The InArray SQL was not correct" + Expect.equal (InArray("", [])).OpSql "?|" "The InArray SQL was not correct" } test "Exists succeeds" { Expect.equal Exists.OpSql "IS NOT NULL" "The Exists SQL was not correct" @@ -101,6 +101,13 @@ let fieldTests = testList "Field" [ Expect.isNone field.ParameterName "The default parameter name should be None" Expect.isNone field.Qualifier "The default table qualifier should be None" } + test "InArray succeeds" { + let field = Field.InArray "ArrayField" "table" [| "z" |] + Expect.equal field.Name "ArrayField" "Field name incorrect" + Expect.equal field.Comparison (InArray("table", [| "z" |])) "Comparison incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } test "Exists succeeds" { let field = Field.Exists "Groovy" Expect.equal field.Name "Groovy" "Field name incorrect" diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index cd37e8c..6043d9c 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -135,60 +135,66 @@ let parametersTests = testList "Parameters" [ /// Unit tests for the Query module of the PostgreSQL library let queryTests = testList "Query" [ testList "whereByFields" [ - test "succeeds for a single field when a logical operator is passed" { + test "succeeds for a single field when a logical comparison is passed" { Expect.equal (Query.whereByFields Any [ { Field.Greater "theField" "0" with ParameterName = Some "@test" } ]) "data->>'theField' > @test" "WHERE clause not correct" } - test "succeeds for a single field when an existence operator is passed" { + test "succeeds for a single field when an existence comparison is passed" { Expect.equal (Query.whereByFields Any [ Field.NotExists "thatField" ]) "data->>'thatField' IS NULL" "WHERE clause not correct" } - test "succeeds for a single field when a between operator is passed with numeric values" { + test "succeeds for a single field when a between comparison is passed with numeric values" { Expect.equal (Query.whereByFields All [ { Field.Between "aField" 50 99 with ParameterName = Some "@range" } ]) "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax" "WHERE clause not correct" } - test "succeeds for a single field when a between operator is passed with non-numeric values" { + test "succeeds for a single field when a between comparison is passed with non-numeric values" { Expect.equal (Query.whereByFields Any [ { Field.Between "field0" "a" "b" with ParameterName = Some "@alpha" } ]) "data->>'field0' BETWEEN @alphamin AND @alphamax" "WHERE clause not correct" } - test "succeeds for all multiple fields with logical operators" { + test "succeeds for all multiple fields with logical comparisons" { Expect.equal (Query.whereByFields All [ Field.Equal "theFirst" "1"; Field.Equal "numberTwo" "2" ]) "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" "WHERE clause not correct" } - test "succeeds for any multiple fields with an existence operator" { + test "succeeds for any multiple fields with an existence comparisons" { Expect.equal (Query.whereByFields Any [ Field.NotExists "thatField"; Field.GreaterOrEqual "thisField" 18 ]) "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0" "WHERE clause not correct" } - test "succeeds for all multiple fields with between operators" { + test "succeeds for all multiple fields with between comparisons" { Expect.equal (Query.whereByFields All [ Field.Between "aField" 50 99; Field.Between "anotherField" "a" "b" ]) "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } - test "succeeds for a field with an IN operator with alphanumeric values" { + test "succeeds for a field with an In comparison with alphanumeric values" { Expect.equal (Query.whereByFields All [ Field.In "this" [ "a"; "b"; "c" ] ]) "data->>'this' IN (@field0_0, @field0_1, @field0_2)" "WHERE clause not correct" } - test "succeeds for a field with an IN operator with numeric values" { + test "succeeds for a field with an In comparison with numeric values" { Expect.equal (Query.whereByFields All [ Field.In "this" [ 7; 14; 21 ] ]) "(data->>'this')::numeric IN (@field0_0, @field0_1, @field0_2)" "WHERE clause not correct" } + test "succeeds for a field with an InArray comparison" { + Expect.equal + (Query.whereByFields All [ Field.InArray "theField" "the_table" [ "q", "r" ] ]) + "data->'theField' ?| @field0" + "WHERE clause not correct" + } ] testList "whereById" [ test "succeeds for numeric ID" { @@ -740,6 +746,26 @@ let findTests = testList "Find" [ PostgresDb.TableName All [ Field.Equal "Value" "mauve"; Field.NotEqual "NumValue" 40 ] Expect.isEmpty docs "There should have been no documents returned" } + testTask "succeeds for InArray when matching documents exist" { + use db = PostgresDb.BuildDb() + do! Definition.ensureTable PostgresDb.TableName + for doc in ArrayDocument.TestDocuments do do! insert PostgresDb.TableName doc + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.InArray "Values" PostgresDb.TableName [ "c" ] ] + Expect.hasLength docs 2 "There should have been two documents returned" + } + testTask "succeeds for InArray when no matching documents exist" { + use db = PostgresDb.BuildDb() + do! Definition.ensureTable PostgresDb.TableName + for doc in ArrayDocument.TestDocuments do do! insert PostgresDb.TableName doc + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.InArray "Values" PostgresDb.TableName [ "j" ] ] + Expect.isEmpty docs "There should have been no documents returned" + } ] testList "byFieldsOrdered" [ testTask "succeeds when sorting ascending" { diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index c2139af..83e1c22 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -15,48 +15,54 @@ open Types /// Unit tests for the Query module of the SQLite library let queryTests = testList "Query" [ testList "whereByFields" [ - test "succeeds for a single field when a logical operator is passed" { + test "succeeds for a single field when a logical comparison is passed" { Expect.equal (Query.whereByFields Any [ { Field.Greater "theField" 0 with ParameterName = Some "@test" } ]) "data->>'theField' > @test" "WHERE clause not correct" } - test "succeeds for a single field when an existence operator is passed" { + test "succeeds for a single field when an existence comparison is passed" { Expect.equal (Query.whereByFields Any [ Field.NotExists "thatField" ]) "data->>'thatField' IS NULL" "WHERE clause not correct" } - test "succeeds for a single field when a between operator is passed" { + test "succeeds for a single field when a between comparison is passed" { Expect.equal (Query.whereByFields All [ { Field.Between "aField" 50 99 with ParameterName = Some "@range" } ]) "data->>'aField' BETWEEN @rangemin AND @rangemax" "WHERE clause not correct" } - test "succeeds for all multiple fields with logical operators" { + test "succeeds for all multiple fields with logical comparisons" { Expect.equal (Query.whereByFields All [ Field.Equal "theFirst" "1"; Field.Equal "numberTwo" "2" ]) "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" "WHERE clause not correct" } - test "succeeds for any multiple fields with an existence operator" { + test "succeeds for any multiple fields with an existence comparison" { Expect.equal (Query.whereByFields Any [ Field.NotExists "thatField"; Field.GreaterOrEqual "thisField" 18 ]) "data->>'thatField' IS NULL OR data->>'thisField' >= @field0" "WHERE clause not correct" } - test "succeeds for all multiple fields with between operators" { + test "succeeds for all multiple fields with between comparisons" { Expect.equal (Query.whereByFields All [ Field.Between "aField" 50 99; Field.Between "anotherField" "a" "b" ]) "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } - test "succeeds for a field with an IN operator" { + test "succeeds for a field with an In comparison" { Expect.equal (Query.whereByFields All [ Field.In "this" [ "a"; "b"; "c" ] ]) "data->>'this' IN (@field0_0, @field0_1, @field0_2)" "WHERE clause not correct" } + test "succeeds for a field with an InArray comparison" { + Expect.equal + (Query.whereByFields All [ Field.InArray "this" "the_table" [ "a"; "b" ] ]) + "EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (@field0_0, @field0_1))" + "WHERE clause not correct" + } ] test "whereById succeeds" { Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" @@ -531,6 +537,26 @@ let findTests = testList "Find" [ let! docs = Find.byFields SqliteDb.TableName Any [ Field.Greater "NumValue" 100 ] Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" } + testTask "succeeds for InArray when matching documents exist" { + use! db = SqliteDb.BuildDb() + do! Definition.ensureTable SqliteDb.TableName + for doc in ArrayDocument.TestDocuments do do! insert SqliteDb.TableName doc + + let! docs = + Find.byFields + SqliteDb.TableName All [ Field.InArray "Values" SqliteDb.TableName [ "c" ] ] + Expect.hasLength docs 2 "There should have been two documents returned" + } + testTask "succeeds for InArray when no matching documents exist" { + use! db = SqliteDb.BuildDb() + do! Definition.ensureTable SqliteDb.TableName + for doc in ArrayDocument.TestDocuments do do! insert SqliteDb.TableName doc + + let! docs = + Find.byFields + SqliteDb.TableName All [ Field.InArray "Values" SqliteDb.TableName [ "j" ] ] + Expect.isEmpty docs "There should have been no documents returned" + } ] testList "byFieldsOrdered" [ testTask "succeeds when sorting ascending" { diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index 0deb585..bec9b16 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -8,20 +8,32 @@ type SubDocument = { Foo: string Bar: string } +type ArrayDocument = + { Id: string + Values: string list } +with + /// + /// A set of documents used for integration tests + /// + static member TestDocuments = + [ { Id = "first"; Values = [ "a"; "b"; "c" ] } + { Id = "second"; Values = [ "c"; "d"; "e" ] } + { Id = "third"; Values = [ "x"; "y"; "z" ] } ] + type JsonDocument = { Id: string Value: string NumValue: int Sub: SubDocument option } + /// An empty JsonDocument let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } /// Documents to use for testing -let testDocuments = [ - { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } - { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } - { Id = "three"; Value = ""; NumValue = 4; Sub = None } - { Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } } - { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } -] +let testDocuments = + [ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } + { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } + { Id = "three"; Value = ""; NumValue = 4; Sub = None } + { Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } } + { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } ]