From e2232e91bbcce30f0bd735b44a6302d86abb808e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Aug 2024 16:39:15 -0400 Subject: [PATCH] Add whereByFields code / tests - Add wrapper class for unnamed field parameters - Support table qualifiers by field - Support dot access to document fields/sub-fields --- src/Common/Library.fs | 26 + src/Postgres/Library.fs | 29 +- src/Sqlite/Library.fs | 27 +- .../BitBadger.Documents.Tests.CSharp.csproj | 1 + src/Tests.CSharp/CommonCSharpTests.cs | 104 +++- src/Tests.CSharp/PostgresCSharpTests.cs | 443 ++++++++++-------- src/Tests.CSharp/SqliteCSharpTests.cs | 306 ++++++------ src/Tests/CommonTests.fs | 50 ++ src/Tests/PostgresTests.fs | 44 ++ src/Tests/SqliteTests.fs | 38 ++ 10 files changed, 695 insertions(+), 373 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index a279b55..564132e 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -97,6 +97,18 @@ type Field = { member this.WithQualifier alias = { this with Qualifier = Some alias } + /// Get the path for this field in PostgreSQL's format + member this.PgSqlPath = + (this.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data") + + if this.Name.Contains '.' then "#>>'{" + String.concat "," (this.Name.Split '.') + "}'" + else $"->>'{this.Name}'" + + /// Get the path for this field in SQLite's format + member this.SqlitePath = + (this.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data") + + if this.Name.Contains '.' then "->>'" + String.concat "'->>'" (this.Name.Split '.') + "'" + else $"->>'{this.Name}'" + /// How fields should be matched [] @@ -107,6 +119,20 @@ type FieldMatch = | All +/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) +type ParameterName() = + /// The counter for the next field value + let mutable currentIdx = -1 + + /// Return the specified name for the parameter, or an anonymous parameter name if none is specified + member this.Derive paramName = + match paramName with + | Some it -> it + | None -> + currentIdx <- currentIdx + 1 + $"@field{currentIdx}" + + /// The required document serialization implementation type IDocumentSerializer = diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index c13641c..2d755e9 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -110,34 +110,27 @@ module Parameters = module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document - [] + [] let whereByFields fields howMatched = - let mutable idx = 0 - let nameField () = - let name = $"field{idx}" - idx <- idx + 1 - name + let name = ParameterName() fields |> List.map (fun it -> - let fieldName = it.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data" - let jsonPath = - if it.Name.Contains '.' then "#>>'{" + String.concat "," (it.Name.Split '.') + "}'" - else $"->>'{it.Name}'" - let column = fieldName + jsonPath match it.Op with - | EX | NEX -> $"{column} {it.Op}" + | EX | NEX -> $"{it.PgSqlPath} {it.Op}" | BT -> - let p = defaultArg it.ParameterName (nameField ()) + let p = name.Derive it.ParameterName let names = $"{p}min AND {p}max" let values = it.Value :?> obj list match values[0] with | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"({column})::numeric {it.Op} {names}" - | _ -> $"{column} {it.Op} {names}" - | _ -> - let p = defaultArg it.ParameterName (nameField ()) - $"{column} {it.Op} {p}") + | :? decimal | :? single | :? double -> $"({it.PgSqlPath})::numeric {it.Op} {names}" + | _ -> $"{it.PgSqlPath} {it.Op} {names}" + | _ -> $"{it.PgSqlPath} {it.Op} {name.Derive it.ParameterName}") |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") + + /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document + let WhereByFields(fields: Field seq, howMatched) = + whereByFields (List.ofSeq fields) howMatched /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 27815f8..cec835d 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -31,20 +31,33 @@ module Configuration = [] module Query = + /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document + [] + let whereByFields fields howMatched = + let name = ParameterName() + fields + |> List.map (fun it -> + match it.Op with + | EX | NEX -> $"{it.SqlitePath} {it.Op}" + | BT -> + let p = name.Derive it.ParameterName + $"{it.SqlitePath} {it.Op} {p}min AND {p}max" + | _ -> $"{it.SqlitePath} {it.Op} {name.Derive it.ParameterName}") + |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") + + /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document + let WhereByFields(fields: Field seq, howMatched) = + whereByFields (List.ofSeq fields) howMatched + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] let whereByField field paramName = - let theRest = - match field.Op with - | EX | NEX -> "" - | BT -> $" {paramName}min AND {paramName}max" - | _ -> $" %s{paramName}" - $"data->>'{field.Name}' {field.Op}{theRest}" + whereByFields [ { field with ParameterName = Some paramName } ] Any /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = - whereByField (Field.EQ (Configuration.idField ()) 0) paramName + whereByFields [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] Any /// Data definition module Definition = diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index 951b6d7..b231560 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -3,6 +3,7 @@ enable enable + latest diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index fd8162a..eca9cfd 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -24,11 +24,11 @@ public static class CommonCSharpTests /// Unit tests /// [Tests] - public static readonly Test Unit = TestList("Common.C# Unit", new[] - { + public static readonly Test Unit = TestList("Common.C# Unit", + [ TestSequenced( - TestList("Configuration", new[] - { + TestList("Configuration", + [ TestCase("UseSerializer succeeds", () => { try @@ -70,9 +70,9 @@ public static class CommonCSharpTests Configuration.UseIdField("Id"); } }) - })), - TestList("Op", new[] - { + ])), + TestList("Op", + [ TestCase("EQ succeeds", () => { Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); @@ -109,9 +109,9 @@ public static class CommonCSharpTests { Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); }) - }), - TestList("Field", new[] - { + ]), + TestList("Field", + [ TestCase("EQ succeeds", () => { var field = Field.EQ("Test", 14); @@ -159,7 +159,7 @@ public static class CommonCSharpTests var field = Field.BT("Age", 18, 49); Expect.equal(field.Name, "Age", "Field name incorrect"); Expect.equal(field.Op, Op.BT, "Operator incorrect"); - Expect.equal(((FSharpList)field.Value).ToArray(), new object[] { 18, 49 }, "Value incorrect"); + Expect.equal(((FSharpList)field.Value).ToArray(), [18, 49], "Value incorrect"); }), TestCase("EX succeeds", () => { @@ -172,25 +172,83 @@ public static class CommonCSharpTests var field = Field.NEX("Rad"); Expect.equal(field.Name, "Rad", "Field name incorrect"); Expect.equal(field.Op, Op.NEX, "Operator incorrect"); - }) - }), - TestList("Query", new[] - { + }), + TestCase("WithParameterName succeeds", () => + { + var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); + Expect.isSome(field.ParameterName, "The parameter name should have been filled"); + Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect"); + }), + TestCase("WithQualifier succeeds", () => + { + var field = Field.EQ("Bill", "Matt").WithQualifier("joe"); + Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); + Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); + }), + TestList("PgSqlPath", + [ + TestCase("succeeds for a single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.PgSqlPath, "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.PgSqlPath, "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data#>>'{My,Nested,Field}'", field.PgSqlPath, "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data#>>'{Nest,Away}'", field.PgSqlPath, "The PostgreSQL path is incorrect"); + }) + ]), + TestList("SqlitePath", + [ + TestCase("succeeds for a single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.SqlitePath, "The SQLite path is incorrect"); + }), + TestCase("succeeds for a single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.SqlitePath, "The SQLite path is incorrect"); + }), + TestCase("succeeds for a nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data->>'My'->>'Nested'->>'Field'", field.SqlitePath, "The SQLite path is incorrect"); + }), + TestCase("succeeds for a nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data->>'Nest'->>'Away'", field.SqlitePath, "The SQLite path is incorrect"); + }) + ]) + ]), + TestList("Query", + [ TestCase("SelectFromTable succeeds", () => { Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", "SELECT statement not correct"); }), - TestList("Definition", new[] - { + TestList("Definition", + [ TestCase("EnsureTableFor succeeds", () => { Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", "CREATE TABLE statement not constructed correctly"); }), - TestList("EnsureKey", new[] - { + TestList("EnsureKey", + [ TestCase("succeeds when a schema is present", () => { Expect.equal(Query.Definition.EnsureKey("test.table"), @@ -203,7 +261,7 @@ public static class CommonCSharpTests "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))", "CREATE INDEX for key statement without schema not constructed correctly"); }) - }), + ]), TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => { Expect.equal( @@ -213,7 +271,7 @@ public static class CommonCSharpTests + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", "CREATE INDEX for multiple field statement incorrect"); }) - }), + ]), TestCase("Insert succeeds", () => { Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); @@ -224,6 +282,6 @@ public static class CommonCSharpTests "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); }) - }) - }); + ]) + ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 9ddd720..960ca6e 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -16,10 +16,10 @@ public static class PostgresCSharpTests /// /// Tests which do not hit the database /// - private static readonly Test Unit = TestList("Unit", new[] - { - TestList("Parameters", new[] - { + private static readonly Test Unit = TestList("Unit", + [ + TestList("Parameters", + [ TestCase("Id succeeds", () => { var it = Parameters.Id(88); @@ -32,38 +32,86 @@ public static class PostgresCSharpTests Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); }), - TestList("AddField", new [] - { + TestList("AddField", + [ TestCase("succeeds when a parameter is added", () => { - var it = Parameters - .AddField("@field", Field.EQ("it", "242"), Enumerable.Empty>()) - .ToList(); + var it = Parameters.AddField("@field", Field.EQ("it", "242"), []).ToList(); Expect.hasLength(it, 1, "There should have been a parameter added"); Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly"); Expect.isTrue(it[0].Item2.IsParameter, "Field parameter value incorrect"); }), TestCase("succeeds when a parameter is not added", () => { - var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty>()); + var it = Parameters.AddField("@it", Field.EX("It"), []); Expect.isEmpty(it, "There should not have been any parameters added"); }), TestCase("succeeds when two parameters are added", () => { - var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), - Enumerable.Empty>()).ToList(); + var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), []).ToList(); Expect.hasLength(it, 2, "There should have been 2 parameters added"); Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct"); Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect"); Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct"); Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); }) - }) - }), - TestList("Query", new[] - { - TestList("WhereByField", new[] - { + ]) + ]), + TestList("Query", + [ + TestList("WhereByFields", + [ + TestCase("succeeds for a single field when a logical operator is passed", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.GT("theField", 0).WithParameterName("@test")], + FieldMatch.Any), + "data->>'theField' > @test", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when an existence operator is passed", () => + { + Expect.equal(Postgres.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any), + "data->>'thatField' IS NULL", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when a between operator is passed with numeric values", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")], + FieldMatch.All), + "(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", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.BT("field0", "a", "b").WithParameterName("@alpha")], + FieldMatch.Any), + "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with logical operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")], + FieldMatch.All), + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); + }), + TestCase("succeeds for any multiple fields with an existence operator", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)], + FieldMatch.Any), + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with between operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")], + FieldMatch.All), + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); + }) + ]), + TestList("WhereByField", + [ TestCase("succeeds when a logical operator is passed", () => { Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"), @@ -84,13 +132,13 @@ public static class PostgresCSharpTests Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"), "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); }) - }), + ]), TestCase("WhereById succeeds", () => { Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); }), - TestList("Definition", new[] - { + TestList("Definition", + [ TestCase("EnsureTable succeeds", () => { Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), @@ -112,7 +160,7 @@ public static class PostgresCSharpTests PostgresDb.TableName), "CREATE INDEX statement not constructed correctly"); }) - }), + ]), TestCase("Update succeeds", () => { Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", @@ -128,8 +176,8 @@ public static class PostgresCSharpTests Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", "WHERE clause not correct"); }), - TestList("Count", new[] - { + TestList("Count", + [ TestCase("All succeeds", () => { Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), @@ -153,9 +201,9 @@ public static class PostgresCSharpTests $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", "JSON Path match count query not correct"); }) - }), - TestList("Exists", new[] - { + ]), + TestList("Exists", + [ TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName), @@ -180,9 +228,9 @@ public static class PostgresCSharpTests $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it", "JSON Path match existence query not correct"); }) - }), - TestList("Find", new[] - { + ]), + TestList("Find", + [ TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName), @@ -207,9 +255,9 @@ public static class PostgresCSharpTests $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", "SELECT by JSON Path match query not correct"); }) - }), - TestList("Patch", new[] - { + ]), + TestList("Patch", + [ TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), @@ -234,9 +282,9 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", "UPDATE partial by JSON Path statement not correct"); }) - }), - TestList("RemoveFields", new[] - { + ]), + TestList("RemoveFields", + [ TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName), @@ -261,9 +309,9 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath", "Remove field by JSON path query not correct"); }) - }), - TestList("Delete", new[] - { + ]), + TestList("Delete", + [ TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName), @@ -288,18 +336,18 @@ public static class PostgresCSharpTests $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", "DELETE by JSON Path match query not correct"); }) - }) - }) - }); + ]) + ]) + ]); - private static readonly List TestDocuments = new() - { + private static readonly List TestDocuments = + [ new() { Id = "one", Value = "FIRST!", NumValue = 0 }, new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, new() { Id = "three", Value = "", NumValue = 4 }, new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } }, new() { Id = "five", Value = "purple", NumValue = 18 } - }; + ]; /// /// Add the test documents to the database @@ -312,10 +360,10 @@ public static class PostgresCSharpTests /// /// Integration tests for the PostgreSQL library /// - private static readonly Test Integration = TestList("Integration", new[] - { - TestList("Configuration", new[] - { + private static readonly Test Integration = TestList("Integration", + [ + TestList("Configuration", + [ TestCase("UseDataSource disposes existing source", () => { using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); @@ -343,11 +391,11 @@ public static class PostgresCSharpTests Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), "Data source should have been the same"); }) - }), - TestList("Custom", new[] - { - TestList("List", new[] - { + ]), + TestList("Custom", + [ + TestList("List", + [ TestCase("succeeds when data is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -368,9 +416,9 @@ public static class PostgresCSharpTests Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("Single", new[] - { + ]), + TestList("Single", + [ TestCase("succeeds when a row is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -390,9 +438,9 @@ public static class PostgresCSharpTests new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("NonQuery", new[] - { + ]), + TestList("NonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = PostgresDb.BuildDb(); @@ -414,7 +462,7 @@ public static class PostgresCSharpTests var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); }) - }), + ]), TestCase("Scalar succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -422,65 +470,71 @@ public static class PostgresCSharpTests var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value")); Expect.equal(nbr, 5, "The query should have returned the number 5"); }) - }), - TestList("Definition", new[] - { + ]), + TestList("Definition", + [ TestCase("EnsureTable succeeds", async () => { await using var db = PostgresDb.BuildDb(); - var tableExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, - Results.ToExists); - var keyExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None, - Results.ToExists); - var exists = await tableExists(); - var alsoExists = await keyExists(); + var exists = await TableExists(); + var alsoExists = await KeyExists(); Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already"); await Definition.EnsureTable("ensured"); - exists = await tableExists(); - alsoExists = await keyExists(); + exists = await TableExists(); + alsoExists = await KeyExists(); Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + Task TableExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, + Results.ToExists); + Task KeyExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None, + Results.ToExists); }), TestCase("EnsureDocumentIndex succeeds", async () => { await using var db = PostgresDb.BuildDb(); - var indexExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", - Parameters.None, Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await Definition.EnsureTable("ensured"); await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); - exists = await indexExists(); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", + Parameters.None, Results.ToExists); }), TestCase("EnsureFieldIndex succeeds", async () => { await using var db = PostgresDb.BuildDb(); - var indexExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None, - Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await Definition.EnsureTable("ensured"); await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None, + Results.ToExists); }) - }), - TestList("Document", new[] - { - TestList("Insert", new[] - { + ]), + TestList("Document", + [ + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -506,9 +560,9 @@ public static class PostgresCSharpTests // This is what should have happened } }) - }), - TestList("Save", new[] - { + ]), + TestList("Save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = PostgresDb.BuildDb(); @@ -537,10 +591,10 @@ public static class PostgresCSharpTests Expect.equal(after.Id, "test", "The document is not correct"); Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); }) - }) - }), - TestList("Count", new[] - { + ]) + ]), + TestList("Count", + [ TestCase("All succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -581,11 +635,11 @@ public static class PostgresCSharpTests var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); Expect.equal(theCount, 3, "There should have been 3 matching documents"); }) - }), - TestList("Exists", new[] - { - TestList("ById", new[] - { + ]), + TestList("Exists", + [ + TestList("ById", + [ TestCase("succeeds when a document exists", async () => { await using var db = PostgresDb.BuildDb(); @@ -602,9 +656,9 @@ public static class PostgresCSharpTests var exists = await Exists.ById(PostgresDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -621,9 +675,9 @@ public static class PostgresCSharpTests var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); Expect.isFalse(exists, "There should not have been existing documents"); }) - }), - TestList("ByContains", new[] - { + ]), + TestList("ByContains", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -640,8 +694,9 @@ public static class PostgresCSharpTests var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("ByJsonPath", new[] { + ]), + TestList("ByJsonPath", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -658,12 +713,12 @@ public static class PostgresCSharpTests var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }) - }), - TestList("Find", new[] - { - TestList("All", new[] - { + ]) + ]), + TestList("Find", + [ + TestList("All", + [ TestCase("succeeds when there is data", async () => { await using var db = PostgresDb.BuildDb(); @@ -681,9 +736,9 @@ public static class PostgresCSharpTests var results = await Find.All(PostgresDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("ById", new[] - { + ]), + TestList("ById", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -701,9 +756,9 @@ public static class PostgresCSharpTests var doc = await Find.ById(PostgresDb.TableName, "three hundred eighty-seven"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -720,9 +775,9 @@ public static class PostgresCSharpTests var docs = await Find.ByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("ByContains", new[] - { + ]), + TestList("ByContains", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -740,9 +795,9 @@ public static class PostgresCSharpTests var docs = await Find.ByContains(PostgresDb.TableName, new { Value = "mauve" }); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("ByJsonPath", new[] - { + ]), + TestList("ByJsonPath", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -759,9 +814,9 @@ public static class PostgresCSharpTests var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FirstByField", new[] - { + ]), + TestList("FirstByField", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -788,9 +843,9 @@ public static class PostgresCSharpTests var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FirstByContains", new[] - { + ]), + TestList("FirstByContains", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -818,9 +873,9 @@ public static class PostgresCSharpTests var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "absent" }); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FirstByJsonPath", new[] - { + ]), + TestList("FirstByJsonPath", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -849,12 +904,12 @@ public static class PostgresCSharpTests var doc = await Find.FirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); Expect.isNull(doc, "There should not have been a document returned"); }) - }) - }), - TestList("Update", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("Update", + [ + TestList("ById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -882,9 +937,9 @@ public static class PostgresCSharpTests await Update.ById(PostgresDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("ByFunc", new[] - { + ]), + TestList("ByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -910,12 +965,12 @@ public static class PostgresCSharpTests await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }) - }), - TestList("Patch", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("Patch", + [ + TestList("ById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -936,9 +991,9 @@ public static class PostgresCSharpTests // This not raising an exception is the test await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -958,9 +1013,9 @@ public static class PostgresCSharpTests // This not raising an exception is the test await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) - }), - TestList("ByContains", new[] - { + ]), + TestList("ByContains", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -980,9 +1035,9 @@ public static class PostgresCSharpTests // This not raising an exception is the test await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); }) - }), - TestList("ByJsonPath", new[] - { + ]), + TestList("ByJsonPath", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -1002,12 +1057,12 @@ public static class PostgresCSharpTests // This not raising an exception is the test await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); }) - }) - }), - TestList("RemoveFields", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("RemoveFields", + [ + TestList("ById", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -1045,9 +1100,9 @@ public static class PostgresCSharpTests // This not raising an exception is the test await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -1087,9 +1142,9 @@ public static class PostgresCSharpTests await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); }) - }), - TestList("ByContains", new[] - { + ]), + TestList("ByContains", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -1129,9 +1184,9 @@ public static class PostgresCSharpTests await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, new[] { "Value" }); }) - }), - TestList("ByJsonPath", new[] - { + ]), + TestList("ByJsonPath", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -1171,12 +1226,12 @@ public static class PostgresCSharpTests await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", new[] { "Value" }); }) - }) - }), - TestList("Delete", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("Delete", + [ + TestList("ById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -1195,9 +1250,9 @@ public static class PostgresCSharpTests var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -1216,9 +1271,9 @@ public static class PostgresCSharpTests var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("ByContains", new[] - { + ]), + TestList("ByContains", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -1237,9 +1292,9 @@ public static class PostgresCSharpTests var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("ByJsonPath", new[] - { + ]), + TestList("ByJsonPath", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -1258,13 +1313,13 @@ public static class PostgresCSharpTests var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }) - }) - }); + ]) + ]) + ]); /// /// All Postgres C# tests /// [Tests] - public static readonly Test All = TestList("Postgres.C#", new[] { Unit, TestSequenced(Integration) }); + public static readonly Test All = TestList("Postgres.C#", [Unit, TestSequenced(Integration)]); } diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 6f38664..44c807a 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -1,5 +1,4 @@ -using System.Text.Json; -using Expecto.CSharp; +using Expecto.CSharp; using Expecto; using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; @@ -17,12 +16,56 @@ public static class SqliteCSharpTests /// /// Unit tests for the SQLite library /// - private static readonly Test Unit = TestList("Unit", new[] - { - TestList("Query", new[] - { - TestList("WhereByField", new[] - { + private static readonly Test Unit = TestList("Unit", + [ + TestList("Query", + [ + TestList("WhereByFields", + [ + TestCase("succeeds for a single field when a logical operator is passed", () => + { + Expect.equal( + Sqlite.Query.WhereByFields([Field.GT("theField", 0).WithParameterName("@test")], + FieldMatch.Any), + "data->>'theField' > @test", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when an existence operator is passed", () => + { + Expect.equal(Sqlite.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any), + "data->>'thatField' IS NULL", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when a between operator is passed", () => + { + Expect.equal( + Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")], + FieldMatch.All), + "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with logical operators", () => + { + Expect.equal( + Sqlite.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")], + FieldMatch.All), + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); + }), + TestCase("succeeds for any multiple fields with an existence operator", () => + { + Expect.equal( + Sqlite.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)], + FieldMatch.Any), + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with between operators", () => + { + Expect.equal( + Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")], + FieldMatch.All), + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); + }) + ]), + TestList("WhereByField", + [ TestCase("succeeds when a logical operator is passed", () => { Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"), @@ -38,7 +81,7 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); }) - }), + ]), TestCase("WhereById succeeds", () => { Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); @@ -53,8 +96,8 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", "UPDATE full statement not correct"); }), - TestList("Count", new[] - { + TestList("Count", + [ TestCase("All succeeds", () => { Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", @@ -66,9 +109,9 @@ public static class SqliteCSharpTests "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field", "JSON field text comparison count query not correct"); }) - }), - TestList("Exists", new[] - { + ]), + TestList("Exists", + [ TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.Exists.ById("tbl"), @@ -81,9 +124,9 @@ public static class SqliteCSharpTests "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it", "JSON field text comparison exists query not correct"); }) - }), - TestList("Find", new[] - { + ]), + TestList("Find", + [ TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", @@ -95,9 +138,9 @@ public static class SqliteCSharpTests "SELECT data FROM tbl WHERE data->>'Golf' >= @field", "SELECT by JSON comparison query not correct"); }) - }), - TestList("Patch", new[] - { + ]), + TestList("Patch", + [ TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.Patch.ById("tbl"), @@ -110,9 +153,9 @@ public static class SqliteCSharpTests "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field", "UPDATE partial by JSON comparison query not correct"); }) - }), - TestList("RemoveFields", new[] - { + ]), + TestList("RemoveFields", + [ TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }), @@ -126,9 +169,9 @@ public static class SqliteCSharpTests "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field", "Remove field by field query not correct"); }) - }), - TestList("Delete", new[] - { + ]), + TestList("Delete", + [ TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", @@ -139,10 +182,10 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Delete.ByField("tbl", Field.NEX("gone")), "DELETE FROM tbl WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct"); }) - }) - }), - TestList("Parameters", new[] - { + ]) + ]), + TestList("Parameters", + [ TestCase("Id succeeds", () => { var theParam = Parameters.Id(7); @@ -157,8 +200,7 @@ public static class SqliteCSharpTests }), TestCase("AddField succeeds when adding a parameter", () => { - var paramList = Parameters.AddField("@field", Field.EQ("it", 99), Enumerable.Empty()) - .ToList(); + var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList(); Expect.hasLength(paramList, 1, "There should have been a parameter added"); var theParam = paramList[0]; Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); @@ -166,25 +208,25 @@ public static class SqliteCSharpTests }), TestCase("AddField succeeds when not adding a parameter", () => { - var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), Enumerable.Empty()); + var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []); Expect.isEmpty(paramSeq, "There should not have been any parameters added"); }), TestCase("None succeeds", () => { Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); }) - }) + ]) // Results are exhaustively executed in the context of other tests - }); + ]); - private static readonly List TestDocuments = new() - { + private static readonly List TestDocuments = + [ new() { Id = "one", Value = "FIRST!", NumValue = 0 }, new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, new() { Id = "three", Value = "", NumValue = 4 }, new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } }, new() { Id = "five", Value = "purple", NumValue = 18 } - }; + ]; /// /// Add the test documents to the database @@ -194,8 +236,8 @@ public static class SqliteCSharpTests foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } - private static readonly Test Integration = TestList("Integration", new[] - { + private static readonly Test Integration = TestList("Integration", + [ TestCase("Configuration.UseConnectionString succeeds", () => { try @@ -209,10 +251,10 @@ public static class SqliteCSharpTests Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); } }), - TestList("Custom", new[] - { - TestList("Single", new[] - { + TestList("Custom", + [ + TestList("Single", + [ TestCase("succeeds when a row is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -232,9 +274,9 @@ public static class SqliteCSharpTests new[] { Parameters.Id("eighty") }, Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("List", new[] - { + ]), + TestList("List", + [ TestCase("succeeds when data is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -254,9 +296,9 @@ public static class SqliteCSharpTests new[] { new SqliteParameter("@value", 100) }, Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("NonQuery", new[] - { + ]), + TestList("NonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -278,7 +320,7 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); }) - }), + ]), TestCase("Scalar succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -286,9 +328,9 @@ public static class SqliteCSharpTests var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); Expect.equal(nbr, 5, "The query should have returned the number 5"); }) - }), - TestList("Definition", new[] - { + ]), + TestList("Definition", + [ TestCase("EnsureTable succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -316,21 +358,23 @@ public static class SqliteCSharpTests TestCase("EnsureFieldIndex succeeds", async () => { await using var db = await SqliteDb.BuildDb(); - var indexExists = () => Custom.Scalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", - Parameters.None, Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await Definition.EnsureTable("ensured"); await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => Custom.Scalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); }) - }), - TestList("Document.Insert", new[] - { + ]), + TestList("Document.Insert", + [ TestCase("succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -355,9 +399,9 @@ public static class SqliteCSharpTests // This is what is supposed to happen } }) - }), - TestList("Document.Save", new[] - { + ]), + TestList("Document.Save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -388,9 +432,9 @@ public static class SqliteCSharpTests Expect.equal(after!.Id, "test", "The updated document is not correct"); Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); }) - }), - TestList("Count", new[] - { + ]), + TestList("Count", + [ TestCase("All succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -415,11 +459,11 @@ public static class SqliteCSharpTests var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple")); Expect.equal(theCount, 1L, "There should have been 1 matching document"); }) - }), - TestList("Exists", new[] - { - TestList("ById", new[] - { + ]), + TestList("Exists", + [ + TestList("ById", + [ TestCase("succeeds when a document exists", async () => { await using var db = await SqliteDb.BuildDb(); @@ -436,9 +480,9 @@ public static class SqliteCSharpTests var exists = await Exists.ById(SqliteDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents exist", async () => { await using var db = await SqliteDb.BuildDb(); @@ -455,12 +499,12 @@ public static class SqliteCSharpTests var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }) - }), - TestList("Find", new[] - { - TestList("All", new[] - { + ]) + ]), + TestList("Find", + [ + TestList("All", + [ TestCase("succeeds when there is data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -478,9 +522,9 @@ public static class SqliteCSharpTests var results = await Find.All(SqliteDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("ById", new[] - { + ]), + TestList("ById", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -498,9 +542,9 @@ public static class SqliteCSharpTests var doc = await Find.ById(SqliteDb.TableName, "twenty two"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents are found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -517,9 +561,9 @@ public static class SqliteCSharpTests var docs = await Find.ByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FirstByField", new[] - { + ]), + TestList("FirstByField", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -546,12 +590,12 @@ public static class SqliteCSharpTests var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); Expect.isNull(doc, "There should not have been a document returned"); }) - }) - }), - TestList("Update", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("Update", + [ + TestList("ById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -577,9 +621,9 @@ public static class SqliteCSharpTests await Update.ById(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("ByFunc", new[] - { + ]), + TestList("ByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -605,12 +649,12 @@ public static class SqliteCSharpTests await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - }), - TestList("Patch", new[] - { - TestList("ById", new[] - { + ]), + ]), + TestList("Patch", + [ + TestList("ById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -632,9 +676,9 @@ public static class SqliteCSharpTests // This not raising an exception is the test await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -654,12 +698,12 @@ public static class SqliteCSharpTests // This not raising an exception is the test await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) - }) - }), - TestList("RemoveFields", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("RemoveFields", + [ + TestList("ById", + [ TestCase("succeeds when fields are removed", async () => { await using var db = await SqliteDb.BuildDb(); @@ -686,9 +730,9 @@ public static class SqliteCSharpTests // This not raising an exception is the test await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when a field is removed", async () => { await using var db = await SqliteDb.BuildDb(); @@ -714,12 +758,12 @@ public static class SqliteCSharpTests // This not raising an exception is the test await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); }) - }) - }), - TestList("Delete", new[] - { - TestList("ById", new[] - { + ]) + ]), + TestList("Delete", + [ + TestList("ById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -738,9 +782,9 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), - TestList("ByField", new[] - { + ]), + TestList("ByField", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -759,14 +803,14 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }) - }), + ]) + ]), TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + ]); /// /// All tests for SQLite C# functions and methods /// [Tests] - public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) }); + public static readonly Test All = TestList("Sqlite.C#", [Unit, TestSequenced(Integration)]); } diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index fd8c5e2..27d6fff 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -119,6 +119,56 @@ let all = Expect.isSome field.Qualifier "The table qualifier should have been filled" Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect" } + testList "PgSqlPath" [ + test "succeeds for a single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" field.PgSqlPath "The PostgreSQL path is incorrect" + } + test "succeeds for a single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" field.PgSqlPath "The PostgreSQL path is incorrect" + } + test "succeeds for a nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data#>>'{My,Nested,Field}'" field.PgSqlPath "The PostgreSQL path is incorrect" + } + test "succeeds for a nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data#>>'{Nest,Away}'" field.PgSqlPath "The PostgreSQL path is incorrect" + } + ] + testList "SqlitePath" [ + test "succeeds for a single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" field.SqlitePath "The SQLite path is incorrect" + } + test "succeeds for a single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" field.SqlitePath "The SQLite path is incorrect" + } + test "succeeds for a nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data->>'My'->>'Nested'->>'Field'" field.SqlitePath "The SQLite path is incorrect" + } + test "succeeds for a nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data->>'Nest'->>'Away'" field.SqlitePath "The SQLite path is incorrect" + } + ] + ] + testList "ParameterName" [ + test "Derive succeeds with existing name" { + let name = ParameterName() + Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco" + Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field" + } + test "Derive succeeds with non-existent name" { + let name = ParameterName() + Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned" + Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call" + Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call" + Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call" + } ] testList "Query" [ test "selectFromTable succeeds" { diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 6849b8b..d25f6ca 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -58,6 +58,50 @@ let unitTests = } ] testList "Query" [ + testList "whereByFields" [ + test "succeeds for a single field when a logical operator is passed" { + Expect.equal + (Query.whereByFields [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ] Any) + "data->>'theField' > @test" + "WHERE clause not correct" + } + test "succeeds for a single field when an existence operator is passed" { + Expect.equal + (Query.whereByFields [ Field.NEX "thatField" ] Any) + "data->>'thatField' IS NULL" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed with numeric values" { + Expect.equal + (Query.whereByFields [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ] All) + "(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" { + Expect.equal + (Query.whereByFields [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ] Any) + "data->>'field0' BETWEEN @alphamin AND @alphamax" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with logical operators" { + Expect.equal + (Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All) + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" + "WHERE clause not correct" + } + test "succeeds for any multiple fields with an existence operator" { + Expect.equal + (Query.whereByFields [ Field.NEX "thatField"; Field.GE "thisField" 18 ] Any) + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with between operators" { + Expect.equal + (Query.whereByFields [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ] All) + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" + } + ] testList "whereByField" [ test "succeeds when a logical operator is passed" { Expect.equal diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 6d97893..73365e4 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -12,6 +12,44 @@ open Types let unitTests = testList "Unit" [ testList "Query" [ + testList "whereByFields" [ + test "succeeds for a single field when a logical operator is passed" { + Expect.equal + (Query.whereByFields [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ] Any) + "data->>'theField' > @test" + "WHERE clause not correct" + } + test "succeeds for a single field when an existence operator is passed" { + Expect.equal + (Query.whereByFields [ Field.NEX "thatField" ] Any) + "data->>'thatField' IS NULL" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed" { + Expect.equal + (Query.whereByFields [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ] All) + "data->>'aField' BETWEEN @rangemin AND @rangemax" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with logical operators" { + Expect.equal + (Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All) + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" + "WHERE clause not correct" + } + test "succeeds for any multiple fields with an existence operator" { + Expect.equal + (Query.whereByFields [ Field.NEX "thatField"; Field.GE "thisField" 18 ] Any) + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with between operators" { + Expect.equal + (Query.whereByFields [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ] All) + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" + } + ] testList "whereByField" [ test "succeeds when a logical operator is passed" { Expect.equal