From 433302d995762aaf8124555e6df9f426b5f0ad8d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 4 Aug 2024 18:59:32 -0400 Subject: [PATCH 01/29] WIP on field enhancements --- src/Common/Library.fs | 32 +++++++++++++++++++++++--------- src/Tests/CommonTests.fs | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 77dc4c1..7dd2993 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -45,43 +45,57 @@ type Field = { /// The value of the field Value: obj + + /// The name of the parameter for this field + ParameterName: string option + + /// The table qualifier for this field + Qualifier: string option } with /// Create an equals (=) field criterion static member EQ name (value: obj) = - { Name = name; Op = EQ; Value = value } + { Name = name; Op = EQ; Value = value; ParameterName = None; Qualifier = None } /// Create a greater than (>) field criterion static member GT name (value: obj) = - { Name = name; Op = GT; Value = value } + { Name = name; Op = GT; Value = value; ParameterName = None; Qualifier = None } /// Create a greater than or equal to (>=) field criterion static member GE name (value: obj) = - { Name = name; Op = GE; Value = value } + { Name = name; Op = GE; Value = value; ParameterName = None; Qualifier = None } /// Create a less than (<) field criterion static member LT name (value: obj) = - { Name = name; Op = LT; Value = value } + { Name = name; Op = LT; Value = value; ParameterName = None; Qualifier = None } /// Create a less than or equal to (<=) field criterion static member LE name (value: obj) = - { Name = name; Op = LE; Value = value } + { Name = name; Op = LE; Value = value; ParameterName = None; Qualifier = None } /// Create a not equals (<>) field criterion static member NE name (value: obj) = - { Name = name; Op = NE; Value = value } + { Name = name; Op = NE; Value = value; ParameterName = None; Qualifier = None } /// Create a BETWEEN field criterion static member BT name (min: obj) (max: obj) = - { Name = name; Op = BT; Value = [ min; max ] } + { Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None } /// Create an exists (IS NOT NULL) field criterion static member EX name = - { Name = name; Op = EX; Value = obj () } + { Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None } /// Create a not exists (IS NULL) field criterion static member NEX name = - { Name = name; Op = NEX; Value = obj () } + { Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None } + + /// Specify the name of the parameter for this field + member this.WithParameterName name = + { this with ParameterName = Some name } + + /// Specify a qualifier (alias) for the table from which this field will be referenced + member this.WithQualifier alias = + { this with Qualifier = Some alias } /// The required document serialization implementation diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index c5265ea..fcc35a2 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -44,52 +44,80 @@ let all = Expect.equal field.Name "Test" "Field name incorrect" Expect.equal field.Op EQ "Operator incorrect" Expect.equal field.Value 14 "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "GT succeeds" { let field = Field.GT "Great" "night" Expect.equal field.Name "Great" "Field name incorrect" Expect.equal field.Op GT "Operator incorrect" Expect.equal field.Value "night" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "GE succeeds" { let field = Field.GE "Nice" 88L Expect.equal field.Name "Nice" "Field name incorrect" Expect.equal field.Op GE "Operator incorrect" Expect.equal field.Value 88L "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "LT succeeds" { let field = Field.LT "Lesser" "seven" Expect.equal field.Name "Lesser" "Field name incorrect" Expect.equal field.Op LT "Operator incorrect" Expect.equal field.Value "seven" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "LE succeeds" { let field = Field.LE "Nobody" "KNOWS"; Expect.equal field.Name "Nobody" "Field name incorrect" Expect.equal field.Op LE "Operator incorrect" Expect.equal field.Value "KNOWS" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "NE succeeds" { let field = Field.NE "Park" "here" Expect.equal field.Name "Park" "Field name incorrect" Expect.equal field.Op NE "Operator incorrect" Expect.equal field.Value "here" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "BT succeeds" { let field = Field.BT "Age" 18 49 Expect.equal field.Name "Age" "Field name incorrect" Expect.equal field.Op BT "Operator incorrect" Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "EX succeeds" { let field = Field.EX "Groovy" Expect.equal field.Name "Groovy" "Field name incorrect" Expect.equal field.Op EX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" } test "NEX succeeds" { let field = Field.NEX "Rad" Expect.equal field.Name "Rad" "Field name incorrect" Expect.equal field.Op NEX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "WithParameterName succeeds" { + let 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" + } + test "WithQualifier succeeds" { + let field = (Field.EQ "Bill" "Matt").WithParameterName "@joe" + Expect.isSome field.Qualifier "The table qualifier should have been filled" + Expect.equal "@joe" field.Qualifier.Value "The table qualifier is incorrect" } ] testList "Query" [ -- 2.45.1 From e96c449324ede0afa689dc6982b1bd796b09f9d6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Aug 2024 08:30:28 -0400 Subject: [PATCH 02/29] WIP on PG Query.whereByFIelds --- src/Common/Library.fs | 9 ++++++++ src/Postgres/Library.fs | 44 ++++++++++++++++++++++++++++++---------- src/Tests/CommonTests.fs | 4 ++-- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 7dd2993..a279b55 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -98,6 +98,15 @@ type Field = { { this with Qualifier = Some alias } +/// How fields should be matched +[] +type FieldMatch = + /// Any field matches (OR) + | Any + /// All fields match (AND) + | All + + /// The required document serialization implementation type IDocumentSerializer = diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index cd551d4..c13641c 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -109,24 +109,46 @@ 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 + 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}" + | BT -> + let p = defaultArg it.ParameterName (nameField ()) + 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}") + |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] + //[] let whereByField field paramName = - match field.Op with - | EX | NEX -> $"data->>'{field.Name}' {field.Op}" - | BT -> - let names = $"{paramName}min AND {paramName}max" - let values = field.Value :?> obj list - match values[0] with - | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"(data->>'{field.Name}')::numeric {field.Op} {names}" - | _ -> $"data->>'{field.Name}' {field.Op} {names}" - | _ -> $"data->>'{field.Name}' {field.Op} %s{paramName}" + 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 /// Table and index definition queries module Definition = diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index fcc35a2..fd8c5e2 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -115,9 +115,9 @@ let all = Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect" } test "WithQualifier succeeds" { - let field = (Field.EQ "Bill" "Matt").WithParameterName "@joe" + let 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" + Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect" } ] testList "Query" [ -- 2.45.1 From e2232e91bbcce30f0bd735b44a6302d86abb808e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Aug 2024 16:39:15 -0400 Subject: [PATCH 03/29] 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 -- 2.45.1 From d131eda56e9b5825e5346aa523a974ef165a01c7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Aug 2024 20:20:10 -0400 Subject: [PATCH 04/29] Remove internal calls to deprecated funcs --- src/Postgres/Library.fs | 105 +++++++++++++++++++++++++------------ src/Sqlite/Library.fs | 112 ++++++++++++++++++++++++++++------------ 2 files changed, 150 insertions(+), 67 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 2d755e9..32aa352 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -62,31 +62,47 @@ module Parameters = [] let jsonParam (name: string) (it: 'TJson) = name, Sql.jsonb (Configuration.serializer().Serialize it) + + /// Convert the fields to their parameters + let private convertFieldsToParameters fields = + let name = ParameterName() + fields + |> Seq.map (fun it -> + seq { + match it.Op with + | EX | NEX -> () + | BT -> + let p = name.Derive it.ParameterName + let values = it.Value :?> obj list + yield ($"{p}min", Sql.parameter (NpgsqlParameter($"{p}min", List.head values))) + yield ($"{p}max", Sql.parameter (NpgsqlParameter($"{p}max", List.last values))) + | _ -> + let p = name.Derive it.ParameterName + yield (p, Sql.parameter (NpgsqlParameter(p, it.Value))) }) + |> Seq.collect id + + /// Create JSON field parameters + [] + let addFieldParams (fields: Field list) parameters = + convertFieldsToParameters fields + |> Seq.toList + |> List.append parameters + + /// Create JSON field parameters + let AddFields fields parameters = + convertFieldsToParameters fields + |> Seq.append parameters /// Create a JSON field parameter [] + [] let addFieldParam name field parameters = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - List.concat - [ parameters - [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values))) - ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] ] - | _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) :: parameters + addFieldParams [ { field with ParameterName = Some name } ] parameters /// Create a JSON field parameter + [] let AddField name field parameters = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - ResizeArray - [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values))) - ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] - |> Seq.append parameters - | _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) |> Seq.singleton |> Seq.append parameters + AddFields [ { field with ParameterName = Some name } ] parameters /// Append JSON field name parameters for the given field names to the given parameters [] @@ -134,7 +150,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] - //[] + [] let whereByField field paramName = whereByFields [ { field with ParameterName = Some paramName } ] Any @@ -184,7 +200,8 @@ module Query = /// Query to count matching documents using a text comparison on a JSON field [] let byField tableName field = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "SELECT COUNT(*) AS it FROM %s WHERE %s" tableName /// Query to count matching documents using a JSON containment query (@>) [] @@ -207,7 +224,8 @@ module Query = /// Query to determine if documents exist using a comparison on a JSON field [] let byField tableName field = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "SELECT EXISTS (SELECT 1 FROM %s WHERE %s) AS it" tableName /// Query to determine if documents exist using a JSON containment query (@>) [] @@ -230,7 +248,8 @@ module Query = /// Query to retrieve documents using a comparison on a JSON field [] let byField tableName field = - $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "%s WHERE %s" (Query.selectFromTable tableName) /// Query to retrieve documents using a JSON containment query (@>) [] @@ -257,7 +276,7 @@ module Query = /// Query to patch documents match a JSON field comparison (->> =) [] let byField tableName field = - whereByField field "@field" |> update tableName + whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName /// Query to patch documents matching a JSON containment query (@>) [] @@ -284,7 +303,7 @@ module Query = /// Query to remove fields from documents via a comparison on a JSON field within the document [] let byField tableName field = - whereByField field "@field" |> update tableName + whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName /// Query to patch documents matching a JSON containment query (@>) [] @@ -307,7 +326,8 @@ module Query = /// Query to delete documents using a comparison on a JSON field [] let byField tableName field = - $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "DELETE FROM %s WHERE %s" tableName /// Query to delete documents using a JSON containment query (@>) [] @@ -444,7 +464,11 @@ module WithProps = /// Count matching documents using a JSON field comparison (->> =) [] let byField tableName field sqlProps = - Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount sqlProps + Custom.scalar + (Query.Count.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + toCount + sqlProps /// Count matching documents using a JSON containment query (@>) [] @@ -468,7 +492,11 @@ module WithProps = /// Determine if a document exists using a JSON field comparison (->> =) [] let byField tableName field sqlProps = - Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists sqlProps + Custom.scalar + (Query.Exists.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + toExists + sqlProps /// Determine if a document exists using a JSON containment query (@>) [] @@ -506,12 +534,18 @@ module WithProps = [] let byField<'TDoc> tableName field sqlProps = Custom.list<'TDoc> - (Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> sqlProps + (Query.Find.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + fromData<'TDoc> + sqlProps /// Retrieve documents matching a JSON field comparison (->> =) let ByField<'TDoc>(tableName, field, sqlProps) = Custom.List<'TDoc>( - Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, sqlProps) + Query.Find.byField tableName field, + addFieldParams [ { field with ParameterName = Some "@field" } ] [], + fromData<'TDoc>, + sqlProps) /// Retrieve documents matching a JSON containment query (@>) [] @@ -540,7 +574,7 @@ module WithProps = let firstByField<'TDoc> tableName field sqlProps = Custom.single<'TDoc> $"{Query.Find.byField tableName field} LIMIT 1" - (addFieldParam "@field" field []) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) fromData<'TDoc> sqlProps @@ -548,7 +582,7 @@ module WithProps = let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = Custom.Single<'TDoc>( $"{Query.Find.byField tableName field} LIMIT 1", - addFieldParam "@field" field [], + addFieldParams [ { field with ParameterName = Some "@field" } ] [], fromData<'TDoc>, sqlProps) @@ -612,7 +646,7 @@ module WithProps = let byField tableName field (patch: 'TPatch) sqlProps = Custom.nonQuery (Query.Patch.byField tableName field) - (addFieldParam "@field" field [ jsonParam "@data" patch ]) + (addFieldParams [ { field with ParameterName = Some "@field" } ] [ jsonParam "@data" patch ]) sqlProps /// Patch documents using a JSON containment query in the WHERE clause (@>) @@ -645,7 +679,7 @@ module WithProps = let byField tableName field fieldNames sqlProps = Custom.nonQuery (Query.RemoveFields.byField tableName field) - (addFieldParam "@field" field [ fieldNameParam fieldNames ]) + (addFieldParams [ { field with ParameterName = Some "@field" } ] [ fieldNameParam fieldNames ]) sqlProps /// Remove fields from documents via a comparison on a JSON field in the document @@ -688,7 +722,10 @@ module WithProps = /// Delete documents by matching a JSON field comparison query (->> =) [] let byField tableName field sqlProps = - Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) sqlProps + Custom.nonQuery + (Query.Delete.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + sqlProps /// Delete documents by matching a JSON contains query (@>) [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index cec835d..90a8520 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -51,6 +51,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] + [] let whereByField field paramName = whereByFields [ { field with ParameterName = Some paramName } ] Any @@ -83,7 +84,8 @@ module Query = /// Query to count matching documents using a text comparison on a JSON field [] let byField tableName field = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "SELECT COUNT(*) AS it FROM %s WHERE %s" tableName /// Queries for determining document existence module Exists = @@ -96,7 +98,8 @@ module Query = /// Query to determine if documents exist using a comparison on a JSON field [] let byField tableName field = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "SELECT EXISTS (SELECT 1 FROM %s WHERE %s) AS it" tableName /// Queries for retrieving documents module Find = @@ -109,7 +112,8 @@ module Query = /// Query to retrieve documents using a comparison on a JSON field [] let byField tableName field = - $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "%s WHERE %s" (Query.selectFromTable tableName) /// Document patching (partial update) queries module Patch = @@ -126,7 +130,7 @@ module Query = /// Query to patch (partially update) a document via a comparison on a JSON field [] let byField tableName field = - whereByField field "@field" |> update tableName + whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName /// Queries to remove fields from documents module RemoveFields = @@ -148,7 +152,7 @@ module Query = /// Query to remove fields from documents via a comparison on a JSON field within the document [] let byField tableName field parameters = - whereByField field "@field" |> update tableName parameters + whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName parameters /// Query to remove fields from documents via a comparison on a JSON field within the document let ByField(tableName, field, parameters) = @@ -165,7 +169,8 @@ module Query = /// Query to delete documents using a comparison on a JSON field [] let byField tableName field = - $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" + whereByFields [ { field with ParameterName = Some "@field" } ] Any + |> sprintf "DELETE FROM %s WHERE %s" tableName /// Parameter handling helpers @@ -182,30 +187,44 @@ module Parameters = let jsonParam name (it: 'TJson) = SqliteParameter(name, Configuration.serializer().Serialize it) + /// Convert the fields to their parameters + let private convertFieldsToParameters fields = + let name = ParameterName() + fields + |> Seq.map (fun it -> + seq { + match it.Op with + | EX | NEX -> () + | BT -> + let p = name.Derive it.ParameterName + let values = it.Value :?> obj list + yield SqliteParameter($"{p}min", List.head values) + yield SqliteParameter($"{p}max", List.last values) + | _ -> yield SqliteParameter(name.Derive it.ParameterName, it.Value) }) + |> Seq.collect id + + /// Create JSON field parameters + [] + let addFieldParams (fields: Field list) parameters = + convertFieldsToParameters fields + |> Seq.toList + |> List.append parameters + + /// Create JSON field parameters + let AddFields fields parameters = + convertFieldsToParameters fields + |> Seq.append parameters + /// Create a JSON field parameter (name "@field") [] + [] let addFieldParam name field parameters = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - SqliteParameter($"{name}min", values[0]) :: SqliteParameter($"{name}max", values[1]) :: parameters - | _ -> SqliteParameter(name, field.Value) :: parameters + addFieldParams [ { field with ParameterName = Some name } ] parameters /// Create a JSON field parameter (name "@field") + [] let AddField(name, field, parameters) = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - // let min = SqliteParameter($"{name}min", SqliteType.Integer) - // min.Value <- values[0] - // let max = SqliteParameter($"{name}max", SqliteType.Integer) - // max.Value <- values[1] - [ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ] - // [min; max] - |> Seq.append parameters - | _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters + AddFields [ { field with ParameterName = Some name } ] parameters /// Append JSON field name parameters for the given field names to the given parameters [] @@ -366,7 +385,11 @@ module WithConn = /// Count matching documents using a comparison on a JSON field [] let byField tableName field conn = - Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount conn + Custom.scalar + (Query.Count.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + toCount + conn /// Commands to determine if documents exist [] @@ -380,7 +403,11 @@ module WithConn = /// Determine if a document exists using a comparison on a JSON field [] let byField tableName field conn = - Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists conn + Custom.scalar + (Query.Exists.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + toExists + conn /// Commands to retrieve documents [] @@ -408,23 +435,35 @@ module WithConn = [] let byField<'TDoc> tableName field conn = Custom.list<'TDoc> - (Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> conn + (Query.Find.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + fromData<'TDoc> + conn /// Retrieve documents via a comparison on a JSON field let ByField<'TDoc>(tableName, field, conn) = Custom.List<'TDoc>( - Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, conn) + Query.Find.byField tableName field, + addFieldParams [ { field with ParameterName = Some "@field" } ] [], + fromData<'TDoc>, + conn) /// Retrieve documents via a comparison on a JSON field, returning only the first result [] let firstByField<'TDoc> tableName field conn = Custom.single - $"{Query.Find.byField tableName field} LIMIT 1" (addFieldParam "@field" field []) fromData<'TDoc> conn + $"{Query.Find.byField tableName field} LIMIT 1" + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + fromData<'TDoc> + conn /// Retrieve documents via a comparison on a JSON field, returning only the first result let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = Custom.Single( - $"{Query.Find.byField tableName field} LIMIT 1", addFieldParam "@field" field [], fromData<'TDoc>, conn) + $"{Query.Find.byField tableName field} LIMIT 1", + addFieldParams [ { field with ParameterName = Some "@field" } ] [], + fromData<'TDoc>, + conn) /// Commands to update documents [] @@ -457,7 +496,9 @@ module WithConn = [] let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) = Custom.nonQuery - (Query.Patch.byField tableName field) (addFieldParam "@field" field [ jsonParam "@data" patch ]) conn + (Query.Patch.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] [ jsonParam "@data" patch ]) + conn /// Commands to remove fields from documents [] @@ -478,7 +519,9 @@ module WithConn = let byField tableName field fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames Custom.nonQuery - (Query.RemoveFields.byField tableName field nameParams) (addFieldParam "@field" field nameParams) conn + (Query.RemoveFields.byField tableName field nameParams) + (addFieldParams [ { field with ParameterName = Some "@field" } ] nameParams) + conn /// Remove fields from documents via a comparison on a JSON field in the document let ByField(tableName, field, fieldNames, conn) = @@ -496,7 +539,10 @@ module WithConn = /// Delete documents by matching a comparison on a JSON field [] let byField tableName field conn = - Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) conn + Custom.nonQuery + (Query.Delete.byField tableName field) + (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + conn /// Commands to execute custom SQL queries -- 2.45.1 From 8a15bdce2ee225456e88cb13dd09ea8398ff3dcb Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Aug 2024 23:23:28 -0400 Subject: [PATCH 05/29] Add byFields to PostgreSQL impl - Swap howMatched and fields throughout - Existing tests pass; still need new ones for byFields --- src/Postgres/Extensions.fs | 82 +++++- src/Postgres/Library.fs | 339 ++++++++++++++++++------ src/Sqlite/Library.fs | 22 +- src/Tests.CSharp/PostgresCSharpTests.cs | 36 +-- src/Tests.CSharp/SqliteCSharpTests.cs | 21 +- src/Tests/PostgresTests.fs | 24 +- src/Tests/SqliteTests.fs | 12 +- 7 files changed, 401 insertions(+), 135 deletions(-) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index c2ae2e5..f6ba32e 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -50,6 +50,11 @@ module Extensions = WithProps.Count.all tableName (Sql.existingConnection conn) /// Count matching documents using a JSON field comparison query (->> =) + member conn.countByFields tableName howMatched fields = + WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) + + /// Count matching documents using a JSON field comparison query (->> =) + [] member conn.countByField tableName field = WithProps.Count.byField tableName field (Sql.existingConnection conn) @@ -66,6 +71,11 @@ module Extensions = WithProps.Exists.byId tableName docId (Sql.existingConnection conn) /// Determine if documents exist using a JSON field comparison query (->> =) + member conn.existsByFields tableName howMatched fields = + WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] member conn.existsByField tableName field = WithProps.Exists.byField tableName field (Sql.existingConnection conn) @@ -86,6 +96,11 @@ module Extensions = WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn) /// Retrieve documents matching a JSON field comparison query (->> =) + member conn.findByFields<'TDoc> tableName howMatched fields = + WithProps.Find.byFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] member conn.findByField<'TDoc> tableName field = WithProps.Find.byField<'TDoc> tableName field (Sql.existingConnection conn) @@ -98,6 +113,11 @@ module Extensions = WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + member conn.findFirstByFields<'TDoc> tableName howMatched fields = + WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] member conn.findFirstByField<'TDoc> tableName field = WithProps.Find.firstByField<'TDoc> tableName field (Sql.existingConnection conn) @@ -122,6 +142,11 @@ module Extensions = WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = + WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] member conn.patchByField tableName field (patch: 'TPatch) = WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) @@ -137,7 +162,12 @@ module Extensions = member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn) + /// Remove fields from documents via a comparison on JSON fields in the document + member conn.removeFieldsByFields tableName howMatched fields fieldNames = + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) + /// Remove fields from documents via a comparison on a JSON field in the document + [] member conn.removeFieldsByField tableName field fieldNames = WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) @@ -153,7 +183,11 @@ module Extensions = member conn.deleteById tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (Sql.existingConnection conn) + member conn.deleteByFields tableName howMatched fields = + WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) + /// Delete documents by matching a JSON field comparison query (->> =) + [] member conn.deleteByField tableName field = WithProps.Delete.byField tableName field (Sql.existingConnection conn) @@ -225,6 +259,12 @@ type NpgsqlConnectionCSharpExtensions = /// Count matching documents using a JSON field comparison query (->> =) [] + static member inline CountByFields(conn, tableName, howMatched, fields) = + WithProps.Count.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Count matching documents using a JSON field comparison query (->> =) + [] + [] static member inline CountByField(conn, tableName, field) = WithProps.Count.byField tableName field (Sql.existingConnection conn) @@ -245,6 +285,12 @@ type NpgsqlConnectionCSharpExtensions = /// Determine if documents exist using a JSON field comparison query (->> =) [] + static member inline ExistsByFields(conn, tableName, howMatched, fields) = + WithProps.Exists.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + [] static member inline ExistsByField(conn, tableName, field) = WithProps.Exists.byField tableName field (Sql.existingConnection conn) @@ -270,6 +316,12 @@ type NpgsqlConnectionCSharpExtensions = /// Retrieve documents matching a JSON field comparison query (->> =) [] + static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = + WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] static member inline FindByField<'TDoc>(conn, tableName, field) = WithProps.Find.ByField<'TDoc>(tableName, field, Sql.existingConnection conn) @@ -283,8 +335,14 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] + static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + [] + [] static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = WithProps.Find.FirstByField<'TDoc>(tableName, field, Sql.existingConnection conn) @@ -315,6 +373,12 @@ type NpgsqlConnectionCSharpExtensions = /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] + static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) = + WithProps.Patch.ByFields(tableName, howMatched, fields, patch, Sql.existingConnection conn) + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) @@ -332,9 +396,15 @@ type NpgsqlConnectionCSharpExtensions = [] static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) = WithProps.RemoveFields.ById(tableName, docId, fieldNames, Sql.existingConnection conn) - + + /// Remove fields from documents via a comparison on JSON fields in the document + [] + static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = + WithProps.RemoveFields.ByFields(tableName, howMatched, fields, fieldNames, Sql.existingConnection conn) + /// Remove fields from documents via a comparison on a JSON field in the document [] + [] static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = WithProps.RemoveFields.ByField(tableName, field, fieldNames, Sql.existingConnection conn) @@ -352,9 +422,15 @@ type NpgsqlConnectionCSharpExtensions = [] static member inline DeleteById(conn, tableName, docId: 'TKey) = WithProps.Delete.byId tableName docId (Sql.existingConnection conn) - + /// Delete documents by matching a JSON field comparison query (->> =) [] + static member inline DeleteByFields(conn, tableName, howMatched, fields) = + WithProps.Delete.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] static member inline DeleteByField(conn, tableName, field) = WithProps.Delete.byField tableName field (Sql.existingConnection conn) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 32aa352..c586168 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -127,7 +127,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document [] - let whereByFields fields howMatched = + let whereByFields howMatched fields = let name = ParameterName() fields |> List.map (fun it -> @@ -145,19 +145,19 @@ module Query = |> 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 + let WhereByFields(howMatched, fields: Field seq) = + whereByFields howMatched (List.ofSeq fields) /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] [] let whereByField field paramName = - whereByFields [ { field with ParameterName = Some paramName } ] Any + whereByFields Any [ { field with ParameterName = Some paramName } ] /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = - whereByFields [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] Any + whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] /// Table and index definition queries module Definition = @@ -197,11 +197,20 @@ module Query = let all tableName = $"SELECT COUNT(*) AS it FROM %s{tableName}" + /// Query to count matching documents using a text comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" + /// Query to count matching documents using a text comparison on a JSON field [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any - |> sprintf "SELECT COUNT(*) AS it FROM %s WHERE %s" tableName + byFields tableName Any [ field ] + + /// Query to count matching documents using a text comparison on JSON fields + let ByFields tableName howMatched fields = + byFields tableName howMatched (List.ofSeq fields) /// Query to count matching documents using a JSON containment query (@>) [] @@ -221,12 +230,21 @@ module Query = let byId tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" + /// Query to determine if documents exist using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByFields howMatched fields}) AS it" + /// Query to determine if documents exist using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any - |> sprintf "SELECT EXISTS (SELECT 1 FROM %s WHERE %s) AS it" tableName + byFields tableName Any [ field ] + /// Query to determine if documents exist using a comparison on JSON fields + let ByFields tableName howMatched fields = + byFields tableName howMatched (List.ofSeq fields) + /// Query to determine if documents exist using a JSON containment query (@>) [] let byContains tableName = @@ -245,11 +263,20 @@ module Query = let byId tableName = $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" + /// Query to retrieve documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"{Query.selectFromTable tableName} WHERE {whereByFields howMatched fields}" + /// Query to retrieve documents using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any - |> sprintf "%s WHERE %s" (Query.selectFromTable tableName) + byFields tableName Any [ field ] + + /// Query to retrieve documents using a comparison on JSON fields + let ByFields tableName howMatched fields = + byFields tableName howMatched (List.ofSeq fields) /// Query to retrieve documents using a JSON containment query (@>) [] @@ -273,11 +300,21 @@ module Query = let byId tableName = whereById "@id" |> update tableName + /// Query to patch documents match JSON field comparisons (->> =) + [] + let byFields tableName howMatched fields = + whereByFields howMatched fields |> update tableName + /// Query to patch documents match a JSON field comparison (->> =) [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName + byFields tableName Any [ field ] + /// Query to patch documents match JSON field comparisons (->> =) + let ByFields tableName howMatched fields = + whereByFields howMatched (List.ofSeq fields) |> update tableName + /// Query to patch documents matching a JSON containment query (@>) [] let byContains tableName = @@ -300,10 +337,20 @@ module Query = let byId tableName = whereById "@id" |> update tableName + /// Query to remove fields from documents via a comparison on JSON fields within the document + [] + let byFields tableName howMatched fields = + whereByFields howMatched fields |> update tableName + /// Query to remove fields from documents via a comparison on a JSON field within the document [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName + byFields tableName Any [ field ] + + /// Query to remove fields from documents via a comparison on JSON fields within the document + let ByFields tableName howMatched fields = + whereByFields howMatched (List.ofSeq fields) |> update tableName /// Query to patch documents matching a JSON containment query (@>) [] @@ -323,11 +370,20 @@ module Query = let byId tableName = $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" + /// Query to delete documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"DELETE FROM %s{tableName} WHERE {whereByFields howMatched fields}" + /// Query to delete documents using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any - |> sprintf "DELETE FROM %s WHERE %s" tableName + byFields tableName Any [ field ] + + /// Query to delete documents using a comparison on JSON fields + let ByFields tableName howMatched fields = + byFields tableName howMatched (List.ofSeq fields) /// Query to delete documents using a JSON containment query (@>) [] @@ -461,14 +517,20 @@ module WithProps = let all tableName sqlProps = Custom.scalar (Query.Count.all tableName) [] toCount sqlProps + /// Count matching documents using JSON field comparisons (->> =) + [] + let byFields tableName howMatched fields sqlProps = + Custom.scalar (Query.Count.byFields tableName howMatched fields) (addFieldParams fields []) toCount sqlProps + /// Count matching documents using a JSON field comparison (->> =) [] + [] let byField tableName field sqlProps = - Custom.scalar - (Query.Count.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - toCount - sqlProps + byFields tableName Any [ field ] sqlProps + + /// Count matching documents using JSON field comparisons (->> =) + let ByFields(tableName, howMatched, fields, sqlProps) = + Custom.Scalar(Query.Count.ByFields tableName howMatched fields, AddFields fields [], toCount, sqlProps) /// Count matching documents using a JSON containment query (@>) [] @@ -489,14 +551,21 @@ module WithProps = let byId tableName (docId: 'TKey) sqlProps = Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps + /// Determine if a document exists using JSON field comparisons (->> =) + [] + let byFields tableName howMatched fields sqlProps = + Custom.scalar + (Query.Exists.byFields tableName howMatched fields) (addFieldParams fields []) toExists sqlProps + /// Determine if a document exists using a JSON field comparison (->> =) [] + [] let byField tableName field sqlProps = - Custom.scalar - (Query.Exists.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - toExists - sqlProps + byFields tableName Any [ field ] sqlProps + + /// Determine if a document exists using JSON field comparisons (->> =) + let ByFields(tableName, howMatched, fields, sqlProps) = + Custom.Scalar(Query.Exists.ByFields tableName howMatched fields, AddFields fields [], toExists, sqlProps) /// Determine if a document exists using a JSON containment query (@>) [] @@ -530,22 +599,27 @@ module WithProps = let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, sqlProps) + /// Retrieve documents matching JSON field comparisons (->> =) + [] + let byFields<'TDoc> tableName howMatched fields sqlProps = + Custom.list<'TDoc> + (Query.Find.byFields tableName howMatched fields) (addFieldParams fields []) fromData<'TDoc> sqlProps + /// Retrieve documents matching a JSON field comparison (->> =) [] + [] let byField<'TDoc> tableName field sqlProps = - Custom.list<'TDoc> - (Query.Find.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - fromData<'TDoc> - sqlProps + byFields<'TDoc> tableName Any [ field ] sqlProps - /// Retrieve documents matching a JSON field comparison (->> =) - let ByField<'TDoc>(tableName, field, sqlProps) = + /// Retrieve documents matching JSON field comparisons (->> =) + let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) = Custom.List<'TDoc>( - Query.Find.byField tableName field, - addFieldParams [ { field with ParameterName = Some "@field" } ] [], - fromData<'TDoc>, - sqlProps) + Query.Find.ByFields tableName howMatched fields, AddFields fields [], fromData<'TDoc>, sqlProps) + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let ByField<'TDoc>(tableName, field, sqlProps) = + ByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) /// Retrieve documents matching a JSON containment query (@>) [] @@ -569,23 +643,34 @@ module WithProps = Custom.List<'TDoc>( Query.Find.byJsonPath tableName, [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) - /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found - [] - let firstByField<'TDoc> tableName field sqlProps = + /// Retrieve the first document matching JSON field comparisons (->> =); returns None if not found + [] + let firstByFields<'TDoc> tableName howMatched fields sqlProps = Custom.single<'TDoc> - $"{Query.Find.byField tableName field} LIMIT 1" - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + $"{Query.Find.byFields tableName howMatched fields} LIMIT 1" + (addFieldParams fields []) fromData<'TDoc> sqlProps - /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + [] + let firstByField<'TDoc> tableName field sqlProps = + firstByFields<'TDoc> tableName Any [ field ] sqlProps + + /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byField tableName field} LIMIT 1", - addFieldParams [ { field with ParameterName = Some "@field" } ] [], + $"{Query.Find.ByFields tableName howMatched fields} LIMIT 1", + AddFields fields [], fromData<'TDoc>, sqlProps) + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = + FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = @@ -642,13 +727,26 @@ module WithProps = Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] sqlProps /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - let byField tableName field (patch: 'TPatch) sqlProps = + [] + let byFields tableName howMatched fields (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] [ jsonParam "@data" patch ]) + (Query.Patch.byFields tableName howMatched fields) + (addFieldParams fields [ jsonParam "@data" patch ]) sqlProps + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) sqlProps = + byFields tableName Any [ field ] patch sqlProps + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + let ByFields(tableName, howMatched, fields, patch: 'TPatch, sqlProps) = + Custom.nonQuery + (Query.Patch.ByFields tableName howMatched fields) + (AddFields fields [ jsonParam "@data" patch ]) + sqlProps + /// Patch documents using a JSON containment query in the WHERE clause (@>) [] let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps = @@ -674,17 +772,31 @@ module WithProps = let ById(tableName, docId: 'TKey, fieldNames, sqlProps) = byId tableName docId (List.ofSeq fieldNames) sqlProps + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames sqlProps = + Custom.nonQuery + (Query.RemoveFields.byFields tableName howMatched fields) + (addFieldParams fields [ fieldNameParam fieldNames ]) + sqlProps + /// Remove fields from documents via a comparison on a JSON field in the document [] + [] let byField tableName field fieldNames sqlProps = - Custom.nonQuery - (Query.RemoveFields.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] [ fieldNameParam fieldNames ]) - sqlProps + byFields tableName Any [ field ] fieldNames sqlProps + /// Remove fields from documents via a comparison on JSON fields in the document + let ByFields(tableName, howMatched, fields, fieldNames, sqlProps) = + Custom.nonQuery + (Query.RemoveFields.ByFields tableName howMatched fields) + (AddFields fields [ FieldName fieldNames ]) + sqlProps + /// Remove fields from documents via a comparison on a JSON field in the document + [] let ByField(tableName, field, fieldNames, sqlProps) = - byField tableName field (List.ofSeq fieldNames) sqlProps + ByFields(tableName, Any, Seq.singleton field, fieldNames, sqlProps) /// Remove fields from documents via a JSON containment query (@>) [] @@ -719,13 +831,19 @@ module WithProps = let byId tableName (docId: 'TKey) sqlProps = Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byFields tableName howMatched fields sqlProps = + Custom.nonQuery (Query.Delete.byFields tableName howMatched fields) (addFieldParams fields []) sqlProps + /// Delete documents by matching a JSON field comparison query (->> =) [] + [] let byField tableName field sqlProps = - Custom.nonQuery - (Query.Delete.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - sqlProps + byFields tableName Any [ field ] sqlProps + + let ByFields(tableName, howMatched, fields, sqlProps) = + Custom.nonQuery (Query.Delete.ByFields tableName howMatched fields) (AddFields fields []) sqlProps /// Delete documents by matching a JSON contains query (@>) [] @@ -819,10 +937,20 @@ module Count = let all tableName = WithProps.Count.all tableName (fromDataSource ()) + /// Count matching documents using a JSON field comparison query (->> =) + [] + let byFields tableName howMatched fields = + WithProps.Count.byFields tableName howMatched fields (fromDataSource ()) + /// Count matching documents using a JSON field comparison query (->> =) [] + [] let byField tableName field = - WithProps.Count.byField tableName field (fromDataSource ()) + byFields tableName Any [ field ] + + /// Count matching documents using a JSON field comparison query (->> =) + let ByFields(tableName, howMatched, fields) = + WithProps.Count.ByFields(tableName, howMatched, fields, fromDataSource ()) /// Count matching documents using a JSON containment query (@>) [] @@ -844,10 +972,20 @@ module Exists = let byId tableName docId = WithProps.Exists.byId tableName docId (fromDataSource ()) + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + let byFields tableName howMatched fields = + WithProps.Exists.byFields tableName howMatched fields (fromDataSource ()) + /// Determine if documents exist using a JSON field comparison query (->> =) [] + [] let byField tableName field = - WithProps.Exists.byField tableName field (fromDataSource ()) + byFields tableName Any [ field ] + + /// Determine if documents exist using a JSON field comparison query (->> =) + let ByFields(tableName, howMatched, fields) = + WithProps.Exists.ByFields(tableName, howMatched, fields, fromDataSource ()) /// Determine if documents exist using a JSON containment query (@>) [] @@ -883,13 +1021,24 @@ module Find = WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) /// Retrieve documents matching a JSON field comparison query (->> =) - [] - let byField<'TDoc> tableName field = - WithProps.Find.byField<'TDoc> tableName field (fromDataSource ()) + [] + let byFields<'TDoc> tableName howMatched fields = + WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ()) /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] + let byField<'TDoc> tableName field = + byFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison query (->> =) + let ByFields<'TDoc>(tableName, howMatched, fields) = + WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] let ByField<'TDoc>(tableName, field) = - WithProps.Find.ByField<'TDoc>(tableName, field, fromDataSource ()) + ByFields<'TDoc>(tableName, Any, Seq.singleton field) /// Retrieve documents matching a JSON containment query (@>) [] @@ -909,14 +1058,25 @@ module Find = let ByJsonPath<'TDoc>(tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] + let firstByFields<'TDoc> tableName howMatched fields = + WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ()) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found [] + [] let firstByField<'TDoc> tableName field = - WithProps.Find.firstByField<'TDoc> tableName field (fromDataSource ()) + firstByFields<'TDoc> tableName Any [ field ] /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = + WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + [] let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = - WithProps.Find.FirstByField<'TDoc>(tableName, field, fromDataSource ()) + FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] @@ -965,10 +1125,20 @@ module Patch = let byId tableName (docId: 'TKey) (patch: 'TPatch) = WithProps.Patch.byId tableName docId patch (fromDataSource ()) + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let byFields tableName howMatched fields (patch: 'TPatch) = + WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ()) + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] + [] let byField tableName field (patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (fromDataSource ()) + byFields tableName Any [ field ] patch + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + let ByFields(tableName, howMatched, fields, patch: 'TPatch) = + WithProps.Patch.ByFields(tableName, howMatched, fields, patch, fromDataSource ()) /// Patch documents using a JSON containment query in the WHERE clause (@>) [] @@ -994,14 +1164,25 @@ module RemoveFields = let ById(tableName, docId: 'TKey, fieldNames) = WithProps.RemoveFields.ById(tableName, docId, fieldNames, fromDataSource ()) - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let byField tableName field fieldNames = - WithProps.RemoveFields.byField tableName field fieldNames (fromDataSource ()) + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames = + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ()) /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames = + byFields tableName Any [ field ] fieldNames + + /// Remove fields from documents via a comparison on JSON fields in the document + let ByFields(tableName, howMatched, fields, fieldNames) = + WithProps.RemoveFields.ByFields(tableName, howMatched, fields, fieldNames, fromDataSource ()) + + /// Remove fields from documents via a comparison on a JSON field in the document + [] let ByField(tableName, field, fieldNames) = - WithProps.RemoveFields.ByField(tableName, field, fieldNames, fromDataSource ()) + ByFields(tableName, Any, Seq.singleton field, fieldNames) /// Remove fields from documents via a JSON containment query (@>) [] @@ -1030,11 +1211,21 @@ module Delete = [] let byId tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (fromDataSource ()) - + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byFields tableName howMatched fields = + WithProps.Delete.byFields tableName howMatched fields (fromDataSource ()) + /// Delete documents by matching a JSON field comparison query (->> =) [] + [] let byField tableName field = - WithProps.Delete.byField tableName field (fromDataSource ()) + byFields tableName Any [ field ] + + /// Delete documents by matching a JSON field comparison query (->> =) + let ByFields(tableName, howMatched, fields) = + WithProps.Delete.ByFields(tableName, howMatched, fields, fromDataSource ()) /// Delete documents by matching a JSON containment query (@>) [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 90a8520..3a52534 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -33,7 +33,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document [] - let whereByFields fields howMatched = + let whereByFields howMatched fields = let name = ParameterName() fields |> List.map (fun it -> @@ -46,19 +46,19 @@ module Query = |> 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 + let WhereByFields(howMatched, fields: Field seq) = + whereByFields howMatched (List.ofSeq fields) /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] [] let whereByField field paramName = - whereByFields [ { field with ParameterName = Some paramName } ] Any + whereByFields Any [ { field with ParameterName = Some paramName } ] /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = - whereByFields [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] Any + whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] /// Data definition module Definition = @@ -84,7 +84,7 @@ module Query = /// Query to count matching documents using a text comparison on a JSON field [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> sprintf "SELECT COUNT(*) AS it FROM %s WHERE %s" tableName /// Queries for determining document existence @@ -98,7 +98,7 @@ module Query = /// Query to determine if documents exist using a comparison on a JSON field [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> sprintf "SELECT EXISTS (SELECT 1 FROM %s WHERE %s) AS it" tableName /// Queries for retrieving documents @@ -112,7 +112,7 @@ module Query = /// Query to retrieve documents using a comparison on a JSON field [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> sprintf "%s WHERE %s" (Query.selectFromTable tableName) /// Document patching (partial update) queries @@ -130,7 +130,7 @@ module Query = /// Query to patch (partially update) a document via a comparison on a JSON field [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> update tableName /// Queries to remove fields from documents module RemoveFields = @@ -152,7 +152,7 @@ module Query = /// Query to remove fields from documents via a comparison on a JSON field within the document [] let byField tableName field parameters = - whereByFields [ { field with ParameterName = Some "@field" } ] Any |> update tableName parameters + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> update tableName parameters /// Query to remove fields from documents via a comparison on a JSON field within the document let ByField(tableName, field, parameters) = @@ -169,7 +169,7 @@ module Query = /// Query to delete documents using a comparison on a JSON field [] let byField tableName field = - whereByFields [ { field with ParameterName = Some "@field" } ] Any + whereByFields Any [ { field with ParameterName = Some "@field" } ] |> sprintf "DELETE FROM %s WHERE %s" tableName diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 960ca6e..fb9f551 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -64,48 +64,48 @@ public static class PostgresCSharpTests 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), + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.GT("theField", 0).WithParameterName("@test")]), "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), + Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), "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), + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.BT("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", () => { Expect.equal( - Postgres.Query.WhereByFields([Field.BT("field0", "a", "b").WithParameterName("@alpha")], - FieldMatch.Any), + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.BT("field0", "a", "b").WithParameterName("@alpha")]), "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), + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), "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), + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.NEX("thatField"), Field.GE("thisField", 18)]), "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), + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", "WHERE clause not correct"); }) @@ -186,7 +186,7 @@ public static class PostgresCSharpTests TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Count.ByField(PostgresDb.TableName, Field.EQ("thatField", 0)), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field", + $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field0", "JSON field text comparison count query not correct"); }), TestCase("ByContains succeeds", () => @@ -213,7 +213,7 @@ public static class PostgresCSharpTests TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Exists.ByField(PostgresDb.TableName, Field.LT("Test", 0)), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it", + $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field0) AS it", "JSON field text comparison exists query not correct"); }), TestCase("ByContains succeeds", () => @@ -240,7 +240,7 @@ public static class PostgresCSharpTests TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)), - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field", + $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field0", "SELECT by JSON comparison query not correct"); }), TestCase("byContains succeeds", () => @@ -267,7 +267,7 @@ public static class PostgresCSharpTests TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field", + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field0", "UPDATE partial by ID statement not correct"); }), TestCase("ByContains succeeds", () => @@ -294,7 +294,7 @@ public static class PostgresCSharpTests TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.RemoveFields.ByField(PostgresDb.TableName, Field.LT("Fly", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field", + $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field0", "Remove field by field query not correct"); }), TestCase("ByContains succeeds", () => diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 44c807a..3b9ac72 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -25,41 +25,40 @@ public static class SqliteCSharpTests 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), + Sqlite.Query.WhereByFields(FieldMatch.Any, + [Field.GT("theField", 0).WithParameterName("@test")]), "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), + Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), "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), + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99).WithParameterName("@range")]), "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), + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), "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), + Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]), "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), + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", "WHERE clause not correct"); }) diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index d25f6ca..d43309b 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -61,43 +61,43 @@ let unitTests = 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) + (Query.whereByFields Any [ { Field.GT "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" { Expect.equal - (Query.whereByFields [ Field.NEX "thatField" ] Any) + (Query.whereByFields Any [ Field.NEX "thatField" ]) "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) + (Query.whereByFields All [ { Field.BT "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" { Expect.equal - (Query.whereByFields [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ] Any) + (Query.whereByFields Any [ { Field.BT "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" { Expect.equal - (Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All) + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) "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) + (Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) "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) + (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } @@ -174,7 +174,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0)) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field" + $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field0" "JSON field text comparison count query not correct" } test "byContains succeeds" { @@ -200,7 +200,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Exists.byField PostgresDb.TableName (Field.LT "Test" 0)) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it" + $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field0) AS it" "JSON field text comparison exists query not correct" } test "byContains succeeds" { @@ -226,7 +226,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0)) - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field" + $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field0" "SELECT by JSON comparison query not correct" } test "byContains succeeds" { @@ -252,7 +252,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field" + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field0" "UPDATE partial by ID statement not correct" } test "byContains succeeds" { @@ -278,7 +278,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0)) - "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field" + "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field0" "Remove field by field query not correct" } test "byContains succeeds" { diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 73365e4..0a3b249 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -15,37 +15,37 @@ let unitTests = 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) + (Query.whereByFields Any [ { Field.GT "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" { Expect.equal - (Query.whereByFields [ Field.NEX "thatField" ] Any) + (Query.whereByFields Any [ Field.NEX "thatField" ]) "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) + (Query.whereByFields All [ { Field.BT "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" { Expect.equal - (Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All) + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) "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) + (Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) "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) + (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } -- 2.45.1 From 202fca272e9673db29cd91eea0a28c9163a29169 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 8 Aug 2024 17:32:44 -0400 Subject: [PATCH 06/29] Prefer seq over list for parameters - This greatly reduces the duplicated functions needed between F# and C#; F# lists satisfy the sequence reqs --- src/Postgres/Extensions.fs | 12 +- src/Postgres/Library.fs | 183 ++++------------ .../PostgresCSharpExtensionTests.cs | 206 ++++++++++-------- src/Tests.CSharp/PostgresCSharpTests.cs | 112 ++++++++++ src/Tests/PostgresExtensionTests.fs | 2 + src/Tests/PostgresTests.fs | 83 ++++++- src/Tests/SqliteTests.fs | 2 + 7 files changed, 354 insertions(+), 246 deletions(-) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index f6ba32e..8e1c7a3 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -260,7 +260,7 @@ type NpgsqlConnectionCSharpExtensions = /// Count matching documents using a JSON field comparison query (->> =) [] static member inline CountByFields(conn, tableName, howMatched, fields) = - WithProps.Count.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) /// Count matching documents using a JSON field comparison query (->> =) [] @@ -286,7 +286,7 @@ type NpgsqlConnectionCSharpExtensions = /// Determine if documents exist using a JSON field comparison query (->> =) [] static member inline ExistsByFields(conn, tableName, howMatched, fields) = - WithProps.Exists.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) /// Determine if documents exist using a JSON field comparison query (->> =) [] @@ -374,7 +374,7 @@ type NpgsqlConnectionCSharpExtensions = /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) = - WithProps.Patch.ByFields(tableName, howMatched, fields, patch, Sql.existingConnection conn) + WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] @@ -400,13 +400,13 @@ type NpgsqlConnectionCSharpExtensions = /// Remove fields from documents via a comparison on JSON fields in the document [] static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = - WithProps.RemoveFields.ByFields(tableName, howMatched, fields, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) /// Remove fields from documents via a comparison on a JSON field in the document [] [] static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - WithProps.RemoveFields.ByField(tableName, field, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) /// Remove fields from documents via a JSON containment query (@>) [] @@ -426,7 +426,7 @@ type NpgsqlConnectionCSharpExtensions = /// Delete documents by matching a JSON field comparison query (->> =) [] static member inline DeleteByFields(conn, tableName, howMatched, fields) = - WithProps.Delete.ByFields(tableName, howMatched, fields, Sql.existingConnection conn) + WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) /// Delete documents by matching a JSON field comparison query (->> =) [] diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index c586168..c4dfc2a 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -63,8 +63,9 @@ module Parameters = let jsonParam (name: string) (it: 'TJson) = name, Sql.jsonb (Configuration.serializer().Serialize it) - /// Convert the fields to their parameters - let private convertFieldsToParameters fields = + /// Create JSON field parameters + [] + let addFieldParams fields parameters = let name = ParameterName() fields |> Seq.map (fun it -> @@ -80,41 +81,22 @@ module Parameters = let p = name.Derive it.ParameterName yield (p, Sql.parameter (NpgsqlParameter(p, it.Value))) }) |> Seq.collect id - - /// Create JSON field parameters - [] - let addFieldParams (fields: Field list) parameters = - convertFieldsToParameters fields - |> Seq.toList - |> List.append parameters - - /// Create JSON field parameters - let AddFields fields parameters = - convertFieldsToParameters fields |> Seq.append parameters + |> Seq.toList + |> Seq.ofList /// Create a JSON field parameter - [] - [] + [] + [] let addFieldParam name field parameters = addFieldParams [ { field with ParameterName = Some name } ] parameters - /// Create a JSON field parameter - [] - let AddField name field parameters = - AddFields [ { field with ParameterName = Some name } ] parameters - /// Append JSON field name parameters for the given field names to the given parameters - [] - let fieldNameParam (fieldNames: string list) = - if fieldNames.Length = 1 then "@name", Sql.string fieldNames[0] - else "@name", Sql.stringArray (Array.ofList fieldNames) - - /// Append JSON field name parameters for the given field names to the given parameters - let FieldName(fieldNames: string seq) = - if Seq.isEmpty fieldNames then "@name", Sql.string (Seq.head fieldNames) + [] + let fieldNameParam (fieldNames: string seq) = + if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames) else "@name", Sql.stringArray (Array.ofSeq fieldNames) - + /// An empty parameter sequence [] let noParams = @@ -126,11 +108,11 @@ module Parameters = module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document - [] + [] let whereByFields howMatched fields = let name = ParameterName() fields - |> List.map (fun it -> + |> Seq.map (fun it -> match it.Op with | EX | NEX -> $"{it.PgSqlPath} {it.Op}" | BT -> @@ -144,10 +126,6 @@ module Query = | _ -> $"{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(howMatched, fields: Field seq) = - whereByFields howMatched (List.ofSeq fields) - /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] [] @@ -198,7 +176,7 @@ module Query = $"SELECT COUNT(*) AS it FROM %s{tableName}" /// Query to count matching documents using a text comparison on JSON fields - [] + [] let byFields tableName howMatched fields = $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" @@ -208,10 +186,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to count matching documents using a text comparison on JSON fields - let ByFields tableName howMatched fields = - byFields tableName howMatched (List.ofSeq fields) - /// Query to count matching documents using a JSON containment query (@>) [] let byContains tableName = @@ -231,7 +205,7 @@ module Query = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" /// Query to determine if documents exist using a comparison on JSON fields - [] + [] let byFields tableName howMatched fields = $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByFields howMatched fields}) AS it" @@ -241,10 +215,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to determine if documents exist using a comparison on JSON fields - let ByFields tableName howMatched fields = - byFields tableName howMatched (List.ofSeq fields) - /// Query to determine if documents exist using a JSON containment query (@>) [] let byContains tableName = @@ -264,7 +234,7 @@ module Query = $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" /// Query to retrieve documents using a comparison on JSON fields - [] + [] let byFields tableName howMatched fields = $"{Query.selectFromTable tableName} WHERE {whereByFields howMatched fields}" @@ -274,10 +244,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to retrieve documents using a comparison on JSON fields - let ByFields tableName howMatched fields = - byFields tableName howMatched (List.ofSeq fields) - /// Query to retrieve documents using a JSON containment query (@>) [] let byContains tableName = @@ -301,7 +267,7 @@ module Query = whereById "@id" |> update tableName /// Query to patch documents match JSON field comparisons (->> =) - [] + [] let byFields tableName howMatched fields = whereByFields howMatched fields |> update tableName @@ -311,10 +277,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to patch documents match JSON field comparisons (->> =) - let ByFields tableName howMatched fields = - whereByFields howMatched (List.ofSeq fields) |> update tableName - /// Query to patch documents matching a JSON containment query (@>) [] let byContains tableName = @@ -338,7 +300,7 @@ module Query = whereById "@id" |> update tableName /// Query to remove fields from documents via a comparison on JSON fields within the document - [] + [] let byFields tableName howMatched fields = whereByFields howMatched fields |> update tableName @@ -348,10 +310,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to remove fields from documents via a comparison on JSON fields within the document - let ByFields tableName howMatched fields = - whereByFields howMatched (List.ofSeq fields) |> update tableName - /// Query to patch documents matching a JSON containment query (@>) [] let byContains tableName = @@ -371,7 +329,7 @@ module Query = $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" /// Query to delete documents using a comparison on JSON fields - [] + [] let byFields tableName howMatched fields = $"DELETE FROM %s{tableName} WHERE {whereByFields howMatched fields}" @@ -381,10 +339,6 @@ module Query = let byField tableName field = byFields tableName Any [ field ] - /// Query to delete documents using a comparison on JSON fields - let ByFields tableName howMatched fields = - byFields tableName howMatched (List.ofSeq fields) - /// Query to delete documents using a JSON containment query (@>) [] let byContains tableName = @@ -424,6 +378,8 @@ module Results = /// Versions of queries that accept SqlProps as the last parameter module WithProps = + module FSharpList = Microsoft.FSharp.Collections.List + /// Commands to execute custom SQL queries [] module Custom = @@ -432,12 +388,12 @@ module WithProps = [] let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps = Sql.query query sqlProps - |> Sql.parameters parameters + |> Sql.parameters (List.ofSeq parameters) |> Sql.executeAsync mapFunc /// Execute a query that returns a list of results let List<'TDoc>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps + let! results = list<'TDoc> query parameters mapFunc.Invoke sqlProps return ResizeArray results } @@ -451,7 +407,7 @@ module WithProps = /// Execute a query that returns one or no results; returns null if not found let Single<'TDoc when 'TDoc: null>( query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! result = single<'TDoc> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps return Option.toObj result } @@ -459,7 +415,7 @@ module WithProps = [] let nonQuery query parameters sqlProps = Sql.query query sqlProps - |> Sql.parameters (FSharp.Collections.List.ofSeq parameters) + |> Sql.parameters (FSharpList.ofSeq parameters) |> Sql.executeNonQueryAsync |> ignoreTask @@ -467,12 +423,12 @@ module WithProps = [] let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = Sql.query query sqlProps - |> Sql.parameters parameters + |> Sql.parameters (FSharpList.ofSeq parameters) |> Sql.executeRowAsync mapFunc /// Execute a query that returns a scalar value let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func, sqlProps) = - scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + scalar<'T> query parameters mapFunc.Invoke sqlProps /// Table and index definition commands module Definition = @@ -518,7 +474,7 @@ module WithProps = Custom.scalar (Query.Count.all tableName) [] toCount sqlProps /// Count matching documents using JSON field comparisons (->> =) - [] + [] let byFields tableName howMatched fields sqlProps = Custom.scalar (Query.Count.byFields tableName howMatched fields) (addFieldParams fields []) toCount sqlProps @@ -528,10 +484,6 @@ module WithProps = let byField tableName field sqlProps = byFields tableName Any [ field ] sqlProps - /// Count matching documents using JSON field comparisons (->> =) - let ByFields(tableName, howMatched, fields, sqlProps) = - Custom.Scalar(Query.Count.ByFields tableName howMatched fields, AddFields fields [], toCount, sqlProps) - /// Count matching documents using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = @@ -552,7 +504,7 @@ module WithProps = Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps /// Determine if a document exists using JSON field comparisons (->> =) - [] + [] let byFields tableName howMatched fields sqlProps = Custom.scalar (Query.Exists.byFields tableName howMatched fields) (addFieldParams fields []) toExists sqlProps @@ -563,10 +515,6 @@ module WithProps = let byField tableName field sqlProps = byFields tableName Any [ field ] sqlProps - /// Determine if a document exists using JSON field comparisons (->> =) - let ByFields(tableName, howMatched, fields, sqlProps) = - Custom.Scalar(Query.Exists.ByFields tableName howMatched fields, AddFields fields [], toExists, sqlProps) - /// Determine if a document exists using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = @@ -614,7 +562,7 @@ module WithProps = /// Retrieve documents matching JSON field comparisons (->> =) let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) = Custom.List<'TDoc>( - Query.Find.ByFields tableName howMatched fields, AddFields fields [], fromData<'TDoc>, sqlProps) + Query.Find.byFields tableName howMatched fields, addFieldParams fields [], fromData<'TDoc>, sqlProps) /// Retrieve documents matching a JSON field comparison (->> =) [] @@ -661,8 +609,8 @@ module WithProps = /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.ByFields tableName howMatched fields} LIMIT 1", - AddFields fields [], + $"{Query.Find.byFields tableName howMatched fields} LIMIT 1", + addFieldParams fields [], fromData<'TDoc>, sqlProps) @@ -727,7 +675,7 @@ module WithProps = Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] sqlProps /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] + [] let byFields tableName howMatched fields (patch: 'TPatch) sqlProps = Custom.nonQuery (Query.Patch.byFields tableName howMatched fields) @@ -740,13 +688,6 @@ module WithProps = let byField tableName field (patch: 'TPatch) sqlProps = byFields tableName Any [ field ] patch sqlProps - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - let ByFields(tableName, howMatched, fields, patch: 'TPatch, sqlProps) = - Custom.nonQuery - (Query.Patch.ByFields tableName howMatched fields) - (AddFields fields [ jsonParam "@data" patch ]) - sqlProps - /// Patch documents using a JSON containment query in the WHERE clause (@>) [] let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps = @@ -773,7 +714,7 @@ module WithProps = byId tableName docId (List.ofSeq fieldNames) sqlProps /// Remove fields from documents via a comparison on JSON fields in the document - [] + [] let byFields tableName howMatched fields fieldNames sqlProps = Custom.nonQuery (Query.RemoveFields.byFields tableName howMatched fields) @@ -781,23 +722,11 @@ module WithProps = sqlProps /// Remove fields from documents via a comparison on a JSON field in the document - [] + [] [] let byField tableName field fieldNames sqlProps = byFields tableName Any [ field ] fieldNames sqlProps - /// Remove fields from documents via a comparison on JSON fields in the document - let ByFields(tableName, howMatched, fields, fieldNames, sqlProps) = - Custom.nonQuery - (Query.RemoveFields.ByFields tableName howMatched fields) - (AddFields fields [ FieldName fieldNames ]) - sqlProps - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let ByField(tableName, field, fieldNames, sqlProps) = - ByFields(tableName, Any, Seq.singleton field, fieldNames, sqlProps) - /// Remove fields from documents via a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) fieldNames sqlProps = @@ -832,7 +761,7 @@ module WithProps = Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps /// Delete documents by matching a JSON field comparison query (->> =) - [] + [] let byFields tableName howMatched fields sqlProps = Custom.nonQuery (Query.Delete.byFields tableName howMatched fields) (addFieldParams fields []) sqlProps @@ -842,9 +771,6 @@ module WithProps = let byField tableName field sqlProps = byFields tableName Any [ field ] sqlProps - let ByFields(tableName, howMatched, fields, sqlProps) = - Custom.nonQuery (Query.Delete.ByFields tableName howMatched fields) (AddFields fields []) sqlProps - /// Delete documents by matching a JSON contains query (@>) [] let byContains tableName (criteria: 'TCriteria) sqlProps = @@ -938,7 +864,7 @@ module Count = WithProps.Count.all tableName (fromDataSource ()) /// Count matching documents using a JSON field comparison query (->> =) - [] + [] let byFields tableName howMatched fields = WithProps.Count.byFields tableName howMatched fields (fromDataSource ()) @@ -948,10 +874,6 @@ module Count = let byField tableName field = byFields tableName Any [ field ] - /// Count matching documents using a JSON field comparison query (->> =) - let ByFields(tableName, howMatched, fields) = - WithProps.Count.ByFields(tableName, howMatched, fields, fromDataSource ()) - /// Count matching documents using a JSON containment query (@>) [] let byContains tableName criteria = @@ -973,7 +895,7 @@ module Exists = WithProps.Exists.byId tableName docId (fromDataSource ()) /// Determine if documents exist using a JSON field comparison query (->> =) - [] + [] let byFields tableName howMatched fields = WithProps.Exists.byFields tableName howMatched fields (fromDataSource ()) @@ -983,10 +905,6 @@ module Exists = let byField tableName field = byFields tableName Any [ field ] - /// Determine if documents exist using a JSON field comparison query (->> =) - let ByFields(tableName, howMatched, fields) = - WithProps.Exists.ByFields(tableName, howMatched, fields, fromDataSource ()) - /// Determine if documents exist using a JSON containment query (@>) [] let byContains tableName criteria = @@ -1126,7 +1044,7 @@ module Patch = WithProps.Patch.byId tableName docId patch (fromDataSource ()) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] + [] let byFields tableName howMatched fields (patch: 'TPatch) = WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ()) @@ -1136,10 +1054,6 @@ module Patch = let byField tableName field (patch: 'TPatch) = byFields tableName Any [ field ] patch - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - let ByFields(tableName, howMatched, fields, patch: 'TPatch) = - WithProps.Patch.ByFields(tableName, howMatched, fields, patch, fromDataSource ()) - /// Patch documents using a JSON containment query in the WHERE clause (@>) [] let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = @@ -1165,25 +1079,16 @@ module RemoveFields = WithProps.RemoveFields.ById(tableName, docId, fieldNames, fromDataSource ()) /// Remove fields from documents via a comparison on JSON fields in the document - [] + [] let byFields tableName howMatched fields fieldNames = WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ()) /// Remove fields from documents via a comparison on a JSON field in the document - [] + [] [] let byField tableName field fieldNames = byFields tableName Any [ field ] fieldNames - /// Remove fields from documents via a comparison on JSON fields in the document - let ByFields(tableName, howMatched, fields, fieldNames) = - WithProps.RemoveFields.ByFields(tableName, howMatched, fields, fieldNames, fromDataSource ()) - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let ByField(tableName, field, fieldNames) = - ByFields(tableName, Any, Seq.singleton field, fieldNames) - /// Remove fields from documents via a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) fieldNames = @@ -1213,7 +1118,7 @@ module Delete = WithProps.Delete.byId tableName docId (fromDataSource ()) /// Delete documents by matching a JSON field comparison query (->> =) - [] + [] let byFields tableName howMatched fields = WithProps.Delete.byFields tableName howMatched fields (fromDataSource ()) @@ -1223,10 +1128,6 @@ module Delete = let byField tableName field = byFields tableName Any [ field ] - /// Delete documents by matching a JSON field comparison query (->> =) - let ByFields(tableName, howMatched, fields) = - WithProps.Delete.ByFields(tableName, howMatched, fields, fromDataSource ()) - /// Delete documents by matching a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) = diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs index b4afe7f..df85d27 100644 --- a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -31,10 +31,10 @@ public class PostgresCSharpExtensionTests /// Integration tests for the SQLite extension methods /// [Tests] - public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[] - { - TestList("CustomList", new[] - { + public static readonly Test Integration = TestList("Postgres.C#.Extensions", + [ + TestList("CustomList", + [ TestCase("succeeds when data is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -57,9 +57,9 @@ public class PostgresCSharpExtensionTests Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("CustomSingle", new[] - { + ]), + TestList("CustomSingle", + [ TestCase("succeeds when a row is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -81,9 +81,9 @@ public class PostgresCSharpExtensionTests new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("CustomNonQuery", new[] - { + ]), + TestList("CustomNonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = PostgresDb.BuildDb(); @@ -107,7 +107,7 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(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(); @@ -169,8 +169,8 @@ public class PostgresCSharpExtensionTests exists = await indexExists(); Expect.isTrue(exists, "The index should now exist"); }), - TestList("Insert", new[] - { + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -198,9 +198,9 @@ public class PostgresCSharpExtensionTests // 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(); @@ -230,7 +230,7 @@ public class PostgresCSharpExtensionTests Expect.isNotNull(after, "There should have been a document returned post-update"); Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); }) - }), + ]), TestCase("CountAll succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -240,6 +240,7 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountAll(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), +#pragma warning disable CS0618 TestCase("CountByField succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -249,6 +250,7 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountByField(PostgresDb.TableName, Field.EQ("Value", "purple")); Expect.equal(theCount, 2, "There should have been 2 matching documents"); }), +#pragma warning restore CS0618 TestCase("CountByContains succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -267,8 +269,8 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); Expect.equal(theCount, 3, "There should have been 3 matching documents"); }), - TestList("ExistsById", new[] - { + TestList("ExistsById", + [ TestCase("succeeds when a document exists", async () => { await using var db = PostgresDb.BuildDb(); @@ -287,9 +289,10 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsById(PostgresDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ExistsByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("ExistsByField", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -308,9 +311,10 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); Expect.isFalse(exists, "There should not have been existing documents"); }) - }), - TestList("ExistsByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("ExistsByContains", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -329,9 +333,9 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" }); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("ExistsByJsonPath", new[] - { + ]), + TestList("ExistsByJsonPath", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -350,9 +354,9 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("FindAll", new[] - { + ]), + TestList("FindAll", + [ TestCase("succeeds when there is data", async () => { await using var db = PostgresDb.BuildDb(); @@ -372,9 +376,9 @@ public class PostgresCSharpExtensionTests var results = await conn.FindAll(PostgresDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("FindById", new[] - { + ]), + TestList("FindById", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -394,9 +398,10 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindById(PostgresDb.TableName, "three hundred eighty-seven"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("FindByField", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -415,9 +420,10 @@ public class PostgresCSharpExtensionTests var docs = await conn.FindByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("FindByContains", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -437,9 +443,9 @@ public class PostgresCSharpExtensionTests var docs = await conn.FindByContains(PostgresDb.TableName, new { Value = "mauve" }); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindByJsonPath", new[] - { + ]), + TestList("FindByJsonPath", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -458,9 +464,10 @@ public class PostgresCSharpExtensionTests var docs = await conn.FindByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindFirstByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("FindFirstByField", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -490,9 +497,10 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindFirstByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("FindFirstByContains", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -523,9 +531,9 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByContains(PostgresDb.TableName, new { Value = "absent" }); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindFirstByJsonPath", new[] - { + ]), + TestList("FindFirstByJsonPath", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -557,9 +565,9 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("UpdateById", new[] - { + ]), + TestList("UpdateById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -588,9 +596,9 @@ public class PostgresCSharpExtensionTests await conn.UpdateById(PostgresDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("UpdateByFunc", new[] - { + ]), + TestList("UpdateByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -617,9 +625,9 @@ public class PostgresCSharpExtensionTests await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - TestList("PatchById", new[] - { + ]), + TestList("PatchById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -641,9 +649,10 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("PatchByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("PatchByField", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -664,9 +673,10 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) - }), - TestList("PatchByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("PatchByContains", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -687,9 +697,9 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); }) - }), - TestList("PatchByJsonPath", new[] - { + ]), + TestList("PatchByJsonPath", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -710,9 +720,9 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); }) - }), - TestList("RemoveFieldsById", new[] - { + ]), + TestList("RemoveFieldsById", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -754,9 +764,10 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Value" }); }) - }), - TestList("RemoveFieldsByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("RemoveFieldsByField", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -800,9 +811,10 @@ public class PostgresCSharpExtensionTests await conn.RemoveFieldsByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); }) - }), - TestList("RemoveFieldsByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("RemoveFieldsByContains", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -846,9 +858,9 @@ public class PostgresCSharpExtensionTests await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, new[] { "Value" }); }) - }), - TestList("RemoveFieldsByJsonPath", new[] - { + ]), + TestList("RemoveFieldsByJsonPath", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); @@ -892,9 +904,9 @@ public class PostgresCSharpExtensionTests await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", new[] { "Value" }); }) - }), - TestList("DeleteById", new[] - { + ]), + TestList("DeleteById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -915,9 +927,10 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("DeleteByField", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -938,9 +951,10 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByContains", new[] - { + ]), +#pragma warning restore CS0618 + TestList("DeleteByContains", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -961,9 +975,9 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByJsonPath", new[] - { + ]), + TestList("DeleteByJsonPath", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -984,6 +998,6 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - }); + ]), + ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index fb9f551..7bf446d 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -32,6 +32,68 @@ 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("AddFields", + [ + TestCase("succeeds when a parameter is added", () => + { + var paramList = Parameters.AddFields([Field.EQ("it", "242")], []).ToList(); + Expect.hasLength(paramList, 1, "There should have been a parameter added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@field0", "Field parameter name not correct"); + if (!value.IsParameter) + { + Expect.isTrue(false, "The parameter was not a Parameter type"); + } + }), + TestCase("succeeds when multiple independent parameters are added", () => + { + var paramList = Parameters.AddFields([Field.EQ("me", "you"), Field.GT("us", "them")], + [Parameters.Id(14)]).ToList(); + Expect.hasLength(paramList, 3, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@id", "First field parameter name not correct"); + if (!value.IsString) + { + Expect.isTrue(false, "First parameter was not a String type"); + } + (name, value) = paramList[1]; + Expect.equal(name, "@field0", "Second field parameter name not correct"); + if (!value.IsParameter) + { + Expect.isTrue(false, "Second parameter was not a Parameter type"); + } + (name, value) = paramList[2]; + Expect.equal(name, "@field1", "Third parameter name not correct"); + if (!value.IsParameter) + { + Expect.isTrue(false, "Third parameter was not a Parameter type"); + } + }), + TestCase("succeeds when a parameter is not added", () => + { + var paramList = Parameters.AddFields([Field.EX("tacos")], []).ToList(); + Expect.isEmpty(paramList, "There should not have been any parameters added"); + }), + TestCase("succeeds when two parameters are added for one field", () => + { + var paramList = + Parameters.AddFields([Field.BT("that", "eh", "zed").WithParameterName("@test")], []).ToList(); + Expect.hasLength(paramList, 2, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@testmin", "Minimum field name not correct"); + if (!value.IsParameter) + { + Expect.isTrue(false, "Minimum parameter was not a Parameter type"); + } + (name, value) = paramList[1]; + Expect.equal(name, "@testmax", "Maximum field name not correct"); + if (!value.IsParameter) + { + Expect.isTrue(false, "Maximum parameter was not a Parameter type"); + } + }) + ]), +#pragma warning disable CS0618 TestList("AddField", [ TestCase("succeeds when a parameter is added", () => @@ -55,6 +117,28 @@ public static class PostgresCSharpTests Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct"); Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); }) + ]), +#pragma warning restore CS0618 + TestList("FieldName", + [ + TestCase("succeeds for one name", () => + { + var (name, value) = Parameters.FieldName(["bob"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsString) + { + Expect.isTrue(false, "The parameter was not a String type"); + } + }), + TestCase("succeeds for multiple names", () => + { + var (name, value) = Parameters.FieldName(["bob", "tom", "mike"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsStringArray) + { + Expect.isTrue(false, "The parameter was not a StringArray type"); + } + }) ]) ]), TestList("Query", @@ -110,6 +194,7 @@ public static class PostgresCSharpTests "WHERE clause not correct"); }) ]), +#pragma warning disable CS0618 TestList("WhereByField", [ TestCase("succeeds when a logical operator is passed", () => @@ -133,6 +218,7 @@ public static class PostgresCSharpTests "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); }) ]), +#pragma warning restore CS0618 TestCase("WhereById succeeds", () => { Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); @@ -183,12 +269,14 @@ public static class PostgresCSharpTests Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Count.ByField(PostgresDb.TableName, Field.EQ("thatField", 0)), $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field0", "JSON field text comparison count query not correct"); }), +#pragma warning restore CS0618 TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), @@ -210,12 +298,14 @@ public static class PostgresCSharpTests $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it", "ID existence query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Exists.ByField(PostgresDb.TableName, Field.LT("Test", 0)), $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field0) AS it", "JSON field text comparison exists query not correct"); }), +#pragma warning restore CS0618 TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), @@ -237,12 +327,14 @@ public static class PostgresCSharpTests $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", "SELECT by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)), $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field0", "SELECT by JSON comparison query not correct"); }), +#pragma warning restore CS0618 TestCase("byContains succeeds", () => { Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), @@ -264,12 +356,14 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id", "UPDATE partial by ID statement not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field0", "UPDATE partial by ID statement not correct"); }), +#pragma warning restore CS0618 TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.Patch.ByContains(PostgresDb.TableName), @@ -291,12 +385,14 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id", "Remove field by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.RemoveFields.ByField(PostgresDb.TableName, Field.LT("Fly", 0)), $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field0", "Remove field by field query not correct"); }), +#pragma warning restore CS0618 TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.RemoveFields.ByContains(PostgresDb.TableName), @@ -318,12 +414,14 @@ public static class PostgresCSharpTests $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", "DELETE by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Postgres.Query.Delete.ByField(PostgresDb.TableName, Field.NEX("gone")), $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct"); }), +#pragma warning restore CS0618 TestCase("byContains succeeds", () => { Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), @@ -603,6 +701,7 @@ public static class PostgresCSharpTests var theCount = await Count.All(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds for numeric range", async () => { await using var db = PostgresDb.BuildDb(); @@ -619,6 +718,7 @@ public static class PostgresCSharpTests var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("Value", "aardvark", "apple")); Expect.equal(theCount, 1, "There should have been 1 matching document"); }), +#pragma warning restore CS0618 TestCase("ByContains succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -657,6 +757,7 @@ public static class PostgresCSharpTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents exist", async () => @@ -676,6 +777,7 @@ public static class PostgresCSharpTests Expect.isFalse(exists, "There should not have been existing documents"); }) ]), +#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents exist", async () => @@ -757,6 +859,7 @@ public static class PostgresCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents are found", async () => @@ -776,6 +879,7 @@ public static class PostgresCSharpTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), +#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents are found", async () => @@ -815,6 +919,7 @@ public static class PostgresCSharpTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), +#pragma warning disable CS0618 TestList("FirstByField", [ TestCase("succeeds when a document is found", async () => @@ -844,6 +949,7 @@ public static class PostgresCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), +#pragma warning restore CS0618 TestList("FirstByContains", [ TestCase("succeeds when a document is found", async () => @@ -992,6 +1098,7 @@ public static class PostgresCSharpTests await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when a document is updated", async () => @@ -1014,6 +1121,7 @@ public static class PostgresCSharpTests await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) ]), +#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when a document is updated", async () => @@ -1101,6 +1209,7 @@ public static class PostgresCSharpTests await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when multiple fields are removed", async () => @@ -1143,6 +1252,7 @@ public static class PostgresCSharpTests new[] { "Value" }); }) ]), +#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when multiple fields are removed", async () => @@ -1251,6 +1361,7 @@ public static class PostgresCSharpTests Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents are deleted", async () => @@ -1272,6 +1383,7 @@ public static class PostgresCSharpTests Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), +#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents are deleted", async () => diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs index 41b1210..b895289 100644 --- a/src/Tests/PostgresExtensionTests.fs +++ b/src/Tests/PostgresExtensionTests.fs @@ -7,6 +7,8 @@ open Expecto open Npgsql open Types +#nowarn "0044" + /// Open a connection to the throwaway database let private mkConn (db: ThrowawayPostgresDb) = let conn = new NpgsqlConnection(db.ConnectionString) diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index d43309b..7683c42 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -5,6 +5,8 @@ open BitBadger.Documents open BitBadger.Documents.Postgres open BitBadger.Documents.Tests +#nowarn "0044" + /// Tests which do not hit the database let unitTests = testList "Unit" [ @@ -18,11 +20,69 @@ let unitTests = ("@test", Sql.jsonb """{"Something":"good"}""") "JSON parameter not constructed correctly" } + testList "addFieldParams" [ + test "succeeds when a parameter is added" { + let paramList = addFieldParams [ Field.EQ "it" "242" ] [] + Expect.hasLength paramList 1 "There should have been a parameter added" + let it = Seq.head paramList + Expect.equal (fst it) "@field0" "Field parameter name not correct" + match snd it with + | SqlValue.Parameter value -> + Expect.equal value.ParameterName "@field0" "Parameter name not correct" + Expect.equal value.Value "242" "Parameter value not correct" + | _ -> Expect.isTrue false "The parameter was not a Parameter type" + } + test "succeeds when multiple independent parameters are added" { + let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ] + Expect.hasLength paramList 3 "There should have been 2 parameters added" + let p = Array.ofSeq paramList + Expect.equal (fst p[0]) "@id" "First field parameter name not correct" + match snd p[0] with + | SqlValue.String value -> + Expect.equal value "14" "First parameter value not correct" + | _ -> Expect.isTrue false "First parameter was not a String type" + Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct" + match snd p[1] with + | SqlValue.Parameter value -> + Expect.equal value.ParameterName "@field0" "Second parameter name not correct" + Expect.equal value.Value "you" "Second parameter value not correct" + | _ -> Expect.isTrue false "Second parameter was not a Parameter type" + Expect.equal (fst p[2]) "@field1" "Third parameter name not correct" + match snd p[2] with + | SqlValue.Parameter value -> + Expect.equal value.ParameterName "@field1" "Third parameter name not correct" + Expect.equal value.Value "them" "Third parameter value not correct" + | _ -> Expect.isTrue false "Third parameter was not a Parameter type" + } + test "succeeds when a parameter is not added" { + let paramList = addFieldParams [ Field.EX "tacos" ] [] + Expect.isEmpty paramList "There should not have been any parameters added" + } + test "succeeds when two parameters are added for one field" { + let paramList = + addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] [] + Expect.hasLength paramList 2 "There should have been 2 parameters added" + let min = Seq.head paramList + Expect.equal (fst min) "@testmin" "Minimum field name not correct" + match snd min with + | SqlValue.Parameter value -> + Expect.equal value.ParameterName "@testmin" "Minimum parameter name not correct" + Expect.equal value.Value "eh" "Minimum parameter value not correct" + | _ -> Expect.isTrue false "Minimum parameter was not a Parameter type" + let max = paramList |> Seq.skip 1 |> Seq.head + Expect.equal (fst max) "@testmax" "Maximum field name not correct" + match snd max with + | SqlValue.Parameter value -> + Expect.equal value.ParameterName "@testmax" "Maximum parameter name not correct" + Expect.equal value.Value "zed" "Maximum parameter value not correct" + | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" + } + ] testList "addFieldParam" [ test "succeeds when a parameter is added" { let paramList = addFieldParam "@field" (Field.EQ "it" "242") [] Expect.hasLength paramList 1 "There should have been a parameter added" - let it = paramList[0] + let it = Seq.head paramList Expect.equal (fst it) "@field" "Field parameter name not correct" match snd it with | SqlValue.Parameter value -> @@ -37,14 +97,14 @@ let unitTests = test "succeeds when two parameters are added" { let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") [] Expect.hasLength paramList 2 "There should have been 2 parameters added" - let min = paramList[0] + let min = Seq.head paramList Expect.equal (fst min) "@fieldmin" "Minimum field name not correct" match snd min with | SqlValue.Parameter value -> Expect.equal value.ParameterName "@fieldmin" "Minimum parameter name not correct" Expect.equal value.Value "eh" "Minimum parameter value not correct" | _ -> Expect.isTrue false "Minimum parameter was not a Parameter type" - let max = paramList[1] + let max = paramList |> Seq.skip 1 |> Seq.head Expect.equal (fst max) "@fieldmax" "Maximum field name not correct" match snd max with | SqlValue.Parameter value -> @@ -53,6 +113,23 @@ let unitTests = | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" } ] + testList "fieldNameParam" [ + test "succeeds for one name" { + let name, value = fieldNameParam [ "bob" ] + Expect.equal name "@name" "The parameter name was incorrect" + match value with + | SqlValue.String it -> Expect.equal it "bob" "The parameter value was incorrect" + | _ -> Expect.isTrue false "The parameter was not a String type" + } + test "succeeds for multiple names" { + let name, value = fieldNameParam [ "bob"; "tom"; "mike" ] + Expect.equal name "@name" "The parameter name was incorrect" + match value with + | SqlValue.StringArray it -> + Expect.equal it [| "bob"; "tom"; "mike" |] "The parameter value was incorrect" + | _ -> Expect.isTrue false "The parameter was not a StringArray type" + } + ] test "noParams succeeds" { Expect.isEmpty noParams "The no-params sequence should be empty" } diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 0a3b249..733d1d8 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -8,6 +8,8 @@ open Expecto open Microsoft.Data.Sqlite open Types +#nowarn "0044" + /// Unit tests for the SQLite library let unitTests = testList "Unit" [ -- 2.45.1 From d8f64417e51d2da2e235f204d1d878aa589aad1d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 8 Aug 2024 19:45:25 -0400 Subject: [PATCH 07/29] Add byFields to SQLite implementation - Add fieldNameParams/FieldNames to Postgres (syncs names between both implementations) --- src/Postgres/Extensions.fs | 29 +- src/Postgres/Library.fs | 20 +- src/Sqlite/Extensions.fs | 116 +++++- src/Sqlite/Library.fs | 370 +++++++++++------- src/Tests.CSharp/PostgresCSharpTests.cs | 2 +- .../SqliteCSharpExtensionTests.cs | 132 ++++--- src/Tests.CSharp/SqliteCSharpTests.cs | 38 +- src/Tests/SqliteTests.fs | 12 +- 8 files changed, 461 insertions(+), 258 deletions(-) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index 8e1c7a3..b297eb9 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -1,5 +1,6 @@ namespace BitBadger.Documents.Postgres +open BitBadger.Documents open Npgsql open Npgsql.FSharp @@ -56,7 +57,7 @@ module Extensions = /// Count matching documents using a JSON field comparison query (->> =) [] member conn.countByField tableName field = - WithProps.Count.byField tableName field (Sql.existingConnection conn) + conn.countByFields tableName Any [ field ] /// Count matching documents using a JSON containment query (@>) member conn.countByContains tableName criteria = @@ -77,7 +78,7 @@ module Extensions = /// Determine if documents exist using a JSON field comparison query (->> =) [] member conn.existsByField tableName field = - WithProps.Exists.byField tableName field (Sql.existingConnection conn) + conn.existsByFields tableName Any [ field ] /// Determine if documents exist using a JSON containment query (@>) member conn.existsByContains tableName criteria = @@ -102,7 +103,7 @@ module Extensions = /// Retrieve documents matching a JSON field comparison query (->> =) [] member conn.findByField<'TDoc> tableName field = - WithProps.Find.byField<'TDoc> tableName field (Sql.existingConnection conn) + conn.findByFields<'TDoc> tableName Any [ field ] /// Retrieve documents matching a JSON containment query (@>) member conn.findByContains<'TDoc> tableName (criteria: obj) = @@ -119,7 +120,7 @@ module Extensions = /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found [] member conn.findFirstByField<'TDoc> tableName field = - WithProps.Find.firstByField<'TDoc> tableName field (Sql.existingConnection conn) + conn.findFirstByFields<'TDoc> tableName Any [ field ] /// Retrieve the first document matching a JSON containment query (@>); returns None if not found member conn.findFirstByContains<'TDoc> tableName (criteria: obj) = @@ -148,7 +149,7 @@ module Extensions = /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] member conn.patchByField tableName field (patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) + conn.patchByFields tableName Any [ field ] patch /// Patch documents using a JSON containment query in the WHERE clause (@>) member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = @@ -169,7 +170,7 @@ module Extensions = /// Remove fields from documents via a comparison on a JSON field in the document [] member conn.removeFieldsByField tableName field fieldNames = - WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) + conn.removeFieldsByFields tableName Any [ field ] fieldNames /// Remove fields from documents via a JSON containment query (@>) member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames = @@ -189,7 +190,7 @@ module Extensions = /// Delete documents by matching a JSON field comparison query (->> =) [] member conn.deleteByField tableName field = - WithProps.Delete.byField tableName field (Sql.existingConnection conn) + conn.deleteByFields tableName Any [ field ] /// Delete documents by matching a JSON containment query (@>) member conn.deleteByContains tableName (criteria: 'TContains) = @@ -266,7 +267,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline CountByField(conn, tableName, field) = - WithProps.Count.byField tableName field (Sql.existingConnection conn) + conn.CountByFields(tableName, Any, [ field ]) /// Count matching documents using a JSON containment query (@>) [] @@ -292,7 +293,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline ExistsByField(conn, tableName, field) = - WithProps.Exists.byField tableName field (Sql.existingConnection conn) + conn.ExistsByFields(tableName, Any, [ field ]) /// Determine if documents exist using a JSON containment query (@>) [] @@ -323,7 +324,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline FindByField<'TDoc>(conn, tableName, field) = - WithProps.Find.ByField<'TDoc>(tableName, field, Sql.existingConnection conn) + conn.FindByFields<'TDoc>(tableName, Any, [ field ]) /// Retrieve documents matching a JSON containment query (@>) [] @@ -344,7 +345,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - WithProps.Find.FirstByField<'TDoc>(tableName, field, Sql.existingConnection conn) + conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ]) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] @@ -380,7 +381,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) + conn.PatchByFields(tableName, Any, [ field ], patch) /// Patch documents using a JSON containment query in the WHERE clause (@>) [] @@ -406,7 +407,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) + conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames) /// Remove fields from documents via a JSON containment query (@>) [] @@ -432,7 +433,7 @@ type NpgsqlConnectionCSharpExtensions = [] [] static member inline DeleteByField(conn, tableName, field) = - WithProps.Delete.byField tableName field (Sql.existingConnection conn) + conn.DeleteByFields(tableName, Any, [ field ]) /// Delete documents by matching a JSON containment query (@>) [] diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index c4dfc2a..4be1a0c 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -90,12 +90,18 @@ module Parameters = [] let addFieldParam name field parameters = addFieldParams [ { field with ParameterName = Some name } ] parameters - + /// Append JSON field name parameters for the given field names to the given parameters - [] - let fieldNameParam (fieldNames: string seq) = + [] + let fieldNameParams (fieldNames: string seq) = if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames) else "@name", Sql.stringArray (Array.ofSeq fieldNames) + + /// Append JSON field name parameters for the given field names to the given parameters + [] + [] + let fieldNameParam fieldNames = + fieldNameParams fieldNames /// An empty parameter sequence [] @@ -707,7 +713,7 @@ module WithProps = /// Remove fields from a document by the document's ID [] let byId tableName (docId: 'TKey) fieldNames sqlProps = - Custom.nonQuery (Query.RemoveFields.byId tableName) [ idParam docId; fieldNameParam fieldNames ] sqlProps + Custom.nonQuery (Query.RemoveFields.byId tableName) [ idParam docId; fieldNameParams fieldNames ] sqlProps /// Remove fields from a document by the document's ID let ById(tableName, docId: 'TKey, fieldNames, sqlProps) = @@ -718,7 +724,7 @@ module WithProps = let byFields tableName howMatched fields fieldNames sqlProps = Custom.nonQuery (Query.RemoveFields.byFields tableName howMatched fields) - (addFieldParams fields [ fieldNameParam fieldNames ]) + (addFieldParams fields [ fieldNameParams fieldNames ]) sqlProps /// Remove fields from documents via a comparison on a JSON field in the document @@ -732,7 +738,7 @@ module WithProps = let byContains tableName (criteria: 'TContains) fieldNames sqlProps = Custom.nonQuery (Query.RemoveFields.byContains tableName) - [ jsonParam "@criteria" criteria; fieldNameParam fieldNames ] + [ jsonParam "@criteria" criteria; fieldNameParams fieldNames ] sqlProps /// Remove fields from documents via a JSON containment query (@>) @@ -744,7 +750,7 @@ module WithProps = let byJsonPath tableName jsonPath fieldNames sqlProps = Custom.nonQuery (Query.RemoveFields.byJsonPath tableName) - [ "@path", Sql.string jsonPath; fieldNameParam fieldNames ] + [ "@path", Sql.string jsonPath; fieldNameParams fieldNames ] sqlProps /// Remove fields from documents via a JSON Path match query (@?) diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 7ad8888..576b771 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -1,5 +1,6 @@ namespace BitBadger.Documents.Sqlite +open BitBadger.Documents open Microsoft.Data.Sqlite /// F# extensions for the SqliteConnection type @@ -44,17 +45,27 @@ module Extensions = member conn.countAll tableName = WithConn.Count.all tableName conn + /// Count matching documents using a comparison on JSON fields + member conn.countByFields tableName howMatched fields = + WithConn.Count.byFields tableName howMatched fields conn + /// Count matching documents using a comparison on a JSON field + [] member conn.countByField tableName field = - WithConn.Count.byField tableName field conn + conn.countByFields tableName Any [ field ] /// Determine if a document exists for the given ID member conn.existsById tableName (docId: 'TKey) = WithConn.Exists.byId tableName docId conn + /// Determine if a document exists using a comparison on JSON fields + member conn.existsByFields tableName howMatched fields = + WithConn.Exists.byFields tableName howMatched fields conn + /// Determine if a document exists using a comparison on a JSON field + [] member conn.existsByField tableName field = - WithConn.Exists.byField tableName field conn + conn.existsByFields tableName Any [ field ] /// Retrieve all documents in the given table member conn.findAll<'TDoc> tableName = @@ -63,14 +74,24 @@ module Extensions = /// Retrieve a document by its ID member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) = WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn - + + /// Retrieve documents via a comparison on JSON fields + member conn.findByFields<'TDoc> tableName howMatched fields = + WithConn.Find.byFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on a JSON field + [] member conn.findByField<'TDoc> tableName field = - WithConn.Find.byField<'TDoc> tableName field conn - + conn.findByFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + member conn.findFirstByFields<'TDoc> tableName howMatched fields = + WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] member conn.findFirstByField<'TDoc> tableName field = - WithConn.Find.firstByField<'TDoc> tableName field conn + conn.findFirstByFields<'TDoc> tableName Any [ field ] /// Update an entire document by its ID member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = @@ -84,25 +105,40 @@ module Extensions = member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) = WithConn.Patch.byId tableName docId patch conn + /// Patch documents using a comparison on JSON fields + member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Patch documents using a comparison on a JSON field + [] member conn.patchByField tableName field (patch: 'TPatch) = - WithConn.Patch.byField tableName field patch conn + conn.patchByFields tableName Any [ field ] patch /// Remove fields from a document by the document's ID member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = WithConn.RemoveFields.byId tableName docId fieldNames conn + /// Remove a field from a document via a comparison on JSON fields in the document + member conn.removeFieldsByFields tableName howMatched fields fieldNames = + WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn + /// Remove a field from a document via a comparison on a JSON field in the document + [] member conn.removeFieldsByField tableName field fieldNames = - WithConn.RemoveFields.byField tableName field fieldNames conn + conn.removeFieldsByFields tableName Any [ field ] fieldNames /// Delete a document by its ID member conn.deleteById tableName (docId: 'TKey) = WithConn.Delete.byId tableName docId conn + /// Delete documents by matching a comparison on JSON fields + member conn.deleteByFields tableName howMatched fields = + WithConn.Delete.byFields tableName howMatched fields conn + /// Delete documents by matching a comparison on a JSON field + [] member conn.deleteByField tableName field = - WithConn.Delete.byField tableName field conn + conn.deleteByFields tableName Any [ field ] open System.Runtime.CompilerServices @@ -157,20 +193,32 @@ type SqliteConnectionCSharpExtensions = static member inline CountAll(conn, tableName) = WithConn.Count.all tableName conn + /// Count matching documents using a comparison on JSON fields + [] + static member inline CountByFields(conn, tableName, howMatched, fields) = + WithConn.Count.byFields tableName howMatched fields conn + /// Count matching documents using a comparison on a JSON field [] + [] static member inline CountByField(conn, tableName, field) = - WithConn.Count.byField tableName field conn + conn.CountByFields(tableName, Any, [ field ]) /// Determine if a document exists for the given ID [] static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) = WithConn.Exists.byId tableName docId conn + /// Determine if a document exists using a comparison on JSON fields + [] + static member inline ExistsByFields(conn, tableName, howMatched, fields) = + WithConn.Exists.byFields tableName howMatched fields conn + /// Determine if a document exists using a comparison on a JSON field [] + [] static member inline ExistsByField(conn, tableName, field) = - WithConn.Exists.byField tableName field conn + conn.ExistsByFields(tableName, Any, [ field ]) /// Retrieve all documents in the given table [] @@ -182,15 +230,27 @@ type SqliteConnectionCSharpExtensions = static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) + /// Retrieve documents via a comparison on JSON fields + [] + static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = + WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on a JSON field [] + [] static member inline FindByField<'TDoc>(conn, tableName, field) = - WithConn.Find.ByField<'TDoc>(tableName, field, conn) + conn.FindByFields<'TDoc>(tableName, Any, [ field ]) + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on a JSON field, returning only the first result [] + [] static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - WithConn.Find.FirstByField<'TDoc>(tableName, field, conn) + conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ]) /// Update an entire document by its ID [] @@ -207,27 +267,45 @@ type SqliteConnectionCSharpExtensions = static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) = WithConn.Patch.byId tableName docId patch conn + /// Patch documents using a comparison on JSON fields + [] + static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) = + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Patch documents using a comparison on a JSON field [] + [] static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) = - WithConn.Patch.byField tableName field patch conn + conn.PatchByFields(tableName, Any, [ field ], patch) /// Remove fields from a document by the document's ID [] static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) = - WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn) - + WithConn.RemoveFields.byId tableName docId fieldNames conn + + /// Remove fields from documents via a comparison on JSON fields in the document + [] + static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = + WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn + /// Remove fields from documents via a comparison on a JSON field in the document [] + [] static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn) + conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames) /// Delete a document by its ID [] static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) = WithConn.Delete.byId tableName docId conn - + + /// Delete documents by matching a comparison on JSON fields + [] + static member inline DeleteByFields(conn, tableName, howMatched, fields) = + WithConn.Delete.byFields tableName howMatched fields conn + /// Delete documents by matching a comparison on a JSON field [] + [] static member inline DeleteByField(conn, tableName, field) = - WithConn.Delete.byField tableName field conn + conn.DeleteByFields(tableName, Any, [ field ]) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 3a52534..86df67b 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -32,11 +32,11 @@ module Configuration = module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document - [] + [] let whereByFields howMatched fields = let name = ParameterName() fields - |> List.map (fun it -> + |> Seq.map (fun it -> match it.Op with | EX | NEX -> $"{it.SqlitePath} {it.Op}" | BT -> @@ -45,10 +45,6 @@ module Query = | _ -> $"{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(howMatched, fields: Field seq) = - whereByFields howMatched (List.ofSeq fields) - /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document [] [] @@ -81,11 +77,16 @@ module Query = let all tableName = $"SELECT COUNT(*) AS it FROM %s{tableName}" + /// Query to count matching documents using a text comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" + /// Query to count matching documents using a text comparison on a JSON field [] + [] let byField tableName field = - whereByFields Any [ { field with ParameterName = Some "@field" } ] - |> sprintf "SELECT COUNT(*) AS it FROM %s WHERE %s" tableName + byFields tableName Any [ field ] /// Queries for determining document existence module Exists = @@ -94,12 +95,17 @@ module Query = [] let byId tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" - + + /// Query to determine if documents exist using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByFields howMatched fields}) AS it" + /// Query to determine if documents exist using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields Any [ { field with ParameterName = Some "@field" } ] - |> sprintf "SELECT EXISTS (SELECT 1 FROM %s WHERE %s) AS it" tableName + byFields tableName Any [ field ] /// Queries for retrieving documents module Find = @@ -109,12 +115,17 @@ module Query = let byId tableName = $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" + /// Query to retrieve documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"{Query.selectFromTable tableName} WHERE {whereByFields howMatched fields}" + /// Query to retrieve documents using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields Any [ { field with ParameterName = Some "@field" } ] - |> sprintf "%s WHERE %s" (Query.selectFromTable tableName) - + byFields tableName Any [ field ] + /// Document patching (partial update) queries module Patch = @@ -126,37 +137,41 @@ module Query = [] let byId tableName = whereById "@id" |> update tableName - + + /// Query to patch (partially update) a document via a comparison on JSON fields + [] + let byFields tableName howMatched fields = + whereByFields howMatched fields |> update tableName + /// Query to patch (partially update) a document via a comparison on a JSON field [] + [] let byField tableName field = - whereByFields Any [ { field with ParameterName = Some "@field" } ] |> update tableName + byFields tableName Any [ field ] /// Queries to remove fields from documents module RemoveFields = /// Create an UPDATE statement to remove parameters - let internal update tableName (parameters: SqliteParameter list) whereClause = - let paramNames = parameters |> List.map _.ParameterName |> String.concat ", " - $"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE {whereClause}" + let internal update tableName (parameters: SqliteParameter seq) whereClause = + let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", " + $"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE %s{whereClause}" /// Query to remove fields from a document by the document's ID - [] + [] let byId tableName parameters = whereById "@id" |> update tableName parameters - /// Query to remove fields from a document by the document's ID - let ById(tableName, parameters) = - byId tableName (List.ofSeq parameters) + /// Query to remove fields from documents via a comparison on JSON fields within the document + [] + let byFields tableName howMatched fields parameters = + whereByFields howMatched fields |> update tableName parameters /// Query to remove fields from documents via a comparison on a JSON field within the document - [] + [] + [] let byField tableName field parameters = - whereByFields Any [ { field with ParameterName = Some "@field" } ] |> update tableName parameters - - /// Query to remove fields from documents via a comparison on a JSON field within the document - let ByField(tableName, field, parameters) = - byField tableName field (List.ofSeq parameters) + byFields tableName Any [ field ] parameters /// Queries to delete documents module Delete = @@ -165,12 +180,17 @@ module Query = [] let byId tableName = $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - + + /// Query to delete documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + $"DELETE FROM %s{tableName} WHERE {whereByFields howMatched fields}" + /// Query to delete documents using a comparison on a JSON field [] + [] let byField tableName field = - whereByFields Any [ { field with ParameterName = Some "@field" } ] - |> sprintf "DELETE FROM %s WHERE %s" tableName + byFields tableName Any [ field ] /// Parameter handling helpers @@ -187,8 +207,9 @@ module Parameters = let jsonParam name (it: 'TJson) = SqliteParameter(name, Configuration.serializer().Serialize it) - /// Convert the fields to their parameters - let private convertFieldsToParameters fields = + /// Create JSON field parameters + [] + let addFieldParams fields parameters = let name = ParameterName() fields |> Seq.map (fun it -> @@ -202,38 +223,23 @@ module Parameters = yield SqliteParameter($"{p}max", List.last values) | _ -> yield SqliteParameter(name.Derive it.ParameterName, it.Value) }) |> Seq.collect id - - /// Create JSON field parameters - [] - let addFieldParams (fields: Field list) parameters = - convertFieldsToParameters fields - |> Seq.toList - |> List.append parameters - - /// Create JSON field parameters - let AddFields fields parameters = - convertFieldsToParameters fields |> Seq.append parameters + |> Seq.toList + |> Seq.ofList /// Create a JSON field parameter (name "@field") - [] + [] [] let addFieldParam name field parameters = addFieldParams [ { field with ParameterName = Some name } ] parameters - /// Create a JSON field parameter (name "@field") - [] - let AddField(name, field, parameters) = - AddFields [ { field with ParameterName = Some name } ] parameters - /// Append JSON field name parameters for the given field names to the given parameters - [] - let fieldNameParams paramName (fieldNames: string list) = - fieldNames |> List.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}")) - - /// Append JSON field name parameters for the given field names to the given parameters - let FieldNames(paramName, fieldNames: string seq) = - fieldNames |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}")) + [] + let fieldNameParams paramName fieldNames = + fieldNames + |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.%s{name}")) + |> Seq.toList + |> Seq.ofList /// An empty parameter sequence [] @@ -382,14 +388,16 @@ module WithConn = let all tableName conn = Custom.scalar (Query.Count.all tableName) [] toCount conn + /// Count matching documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.scalar (Query.Count.byFields tableName howMatched fields) (addFieldParams fields []) toCount conn + /// Count matching documents using a comparison on a JSON field [] + [] let byField tableName field conn = - Custom.scalar - (Query.Count.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - toCount - conn + byFields tableName Any [ field ] conn /// Commands to determine if documents exist [] @@ -399,15 +407,17 @@ module WithConn = [] let byId tableName (docId: 'TKey) conn = Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists conn - + + /// Determine if a document exists using a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.scalar (Query.Exists.byFields tableName howMatched fields) (addFieldParams fields []) toExists conn + /// Determine if a document exists using a comparison on a JSON field [] + [] let byField tableName field conn = - Custom.scalar - (Query.Exists.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - toExists - conn + byFields tableName Any [ field ] conn /// Commands to retrieve documents [] @@ -430,40 +440,56 @@ module WithConn = /// Retrieve a document by its ID (returns null if not found) let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn) - + + /// Retrieve documents via a comparison on JSON fields + [] + let byFields<'TDoc> tableName howMatched fields conn = + Custom.list<'TDoc> + (Query.Find.byFields tableName howMatched fields) (addFieldParams fields []) fromData<'TDoc> conn + /// Retrieve documents via a comparison on a JSON field [] + [] let byField<'TDoc> tableName field conn = - Custom.list<'TDoc> - (Query.Find.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - fromData<'TDoc> - conn + byFields<'TDoc> tableName Any [ field ] conn - /// Retrieve documents via a comparison on a JSON field - let ByField<'TDoc>(tableName, field, conn) = + /// Retrieve documents via a comparison on JSON fields + let ByFields<'TDoc>(tableName, howMatched, fields, conn) = Custom.List<'TDoc>( - Query.Find.byField tableName field, - addFieldParams [ { field with ParameterName = Some "@field" } ] [], - fromData<'TDoc>, - conn) + Query.Find.byFields tableName howMatched fields, addFieldParams fields [], fromData<'TDoc>, conn) + + /// Retrieve documents via a comparison on a JSON field + [] + let ByField<'TDoc>(tableName, field, conn) = + ByFields<'TDoc>(tableName, Any, [ field ], conn) - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - let firstByField<'TDoc> tableName field conn = + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + let firstByFields<'TDoc> tableName howMatched fields conn = Custom.single - $"{Query.Find.byField tableName field} LIMIT 1" - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) + $"{Query.Find.byFields tableName howMatched fields} LIMIT 1" + (addFieldParams fields []) fromData<'TDoc> conn - + /// Retrieve documents via a comparison on a JSON field, returning only the first result - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + [] + [] + let firstByField<'TDoc> tableName field conn = + firstByFields<'TDoc> tableName Any [ field ] conn + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = Custom.Single( - $"{Query.Find.byField tableName field} LIMIT 1", - addFieldParams [ { field with ParameterName = Some "@field" } ] [], + $"{Query.Find.byFields tableName howMatched fields} LIMIT 1", + addFieldParams fields [], fromData<'TDoc>, conn) + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + FirstByFields(tableName, Any, [ field ], conn) /// Commands to update documents [] @@ -492,40 +518,47 @@ module WithConn = let byId tableName (docId: 'TKey) (patch: 'TPatch) conn = Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] conn + /// Patch documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields (patch: 'TPatch) conn = + Custom.nonQuery + (Query.Patch.byFields tableName howMatched fields) + (addFieldParams fields [ jsonParam "@data" patch ]) + conn + /// Patch documents using a comparison on a JSON field [] - let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) = - Custom.nonQuery - (Query.Patch.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] [ jsonParam "@data" patch ]) - conn + [] + let byField tableName field (patch: 'TPatch) conn = + byFields tableName Any [ field ] patch conn /// Commands to remove fields from documents [] module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames conn = - let nameParams = fieldNameParams "@name" fieldNames - Custom.nonQuery (Query.RemoveFields.byId tableName nameParams) (idParam docId :: nameParams) conn - - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames, conn) = - byId tableName docId (List.ofSeq fieldNames) conn - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let byField tableName field fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames Custom.nonQuery - (Query.RemoveFields.byField tableName field nameParams) - (addFieldParams [ { field with ParameterName = Some "@field" } ] nameParams) + (Query.RemoveFields.byId tableName nameParams) + (idParam docId |> Seq.singleton |> Seq.append nameParams) conn + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames conn = + let nameParams = fieldNameParams "@name" fieldNames + Custom.nonQuery + (Query.RemoveFields.byFields tableName howMatched fields nameParams) + (addFieldParams fields nameParams) + conn + /// Remove fields from documents via a comparison on a JSON field in the document - let ByField(tableName, field, fieldNames, conn) = - byField tableName field (List.ofSeq fieldNames) conn + [] + [] + let byField tableName field fieldNames conn = + byFields tableName Any [ field ] fieldNames conn /// Commands to delete documents [] @@ -535,14 +568,17 @@ module WithConn = [] let byId tableName (docId: 'TKey) conn = Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn - + + /// Delete documents by matching a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.nonQuery (Query.Delete.byFields tableName howMatched fields) (addFieldParams fields []) conn + /// Delete documents by matching a comparison on a JSON field [] + [] let byField tableName field conn = - Custom.nonQuery - (Query.Delete.byField tableName field) - (addFieldParams [ { field with ParameterName = Some "@field" } ] []) - conn + byFields tableName Any [ field ] conn /// Commands to execute custom SQL queries @@ -630,11 +666,17 @@ module Count = use conn = Configuration.dbConn () WithConn.Count.all tableName conn + /// Count matching documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Count.byFields tableName howMatched fields conn + /// Count matching documents using a comparison on a JSON field [] + [] let byField tableName field = - use conn = Configuration.dbConn () - WithConn.Count.byField tableName field conn + byFields tableName Any [ field ] /// Commands to determine if documents exist [] @@ -645,12 +687,18 @@ module Exists = let byId tableName (docId: 'TKey) = use conn = Configuration.dbConn () WithConn.Exists.byId tableName docId conn - + + /// Determine if a document exists using a comparison on JSON fields + [] + let byFields tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Exists.byFields tableName howMatched fields conn + /// Determine if a document exists using a comparison on a JSON field [] + [] let byField tableName field = - use conn = Configuration.dbConn () - WithConn.Exists.byField tableName field conn + byFields tableName Any [ field ] /// Commands to determine if documents exist [] @@ -678,27 +726,49 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) + /// Retrieve documents via a comparison on JSON fields + [] + let byFields<'TDoc> tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Find.byFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on a JSON field [] + [] let byField<'TDoc> tableName field = + byFields tableName Any [ field ] + + /// Retrieve documents via a comparison on JSON fields + let ByFields<'TDoc>(tableName, howMatched, fields) = use conn = Configuration.dbConn () - WithConn.Find.byField<'TDoc> tableName field conn - + WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on a JSON field + [] let ByField<'TDoc>(tableName, field) = - use conn = Configuration.dbConn () - WithConn.Find.ByField<'TDoc>(tableName, field, conn) + ByFields<'TDoc>(tableName, Any, [ field ]) + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + let firstByFields<'TDoc> tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on a JSON field, returning only the first result [] + [] let firstByField<'TDoc> tableName field = + firstByFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = use conn = Configuration.dbConn () - WithConn.Find.firstByField<'TDoc> tableName field conn - + WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = - use conn = Configuration.dbConn () - WithConn.Find.FirstByField<'TDoc>(tableName, field, conn) + FirstByFields<'TDoc>(tableName, Any, [ field ]) /// Commands to update documents [] @@ -731,37 +801,39 @@ module Patch = use conn = Configuration.dbConn () WithConn.Patch.byId tableName docId patch conn + /// Patch documents using a comparison on JSON fields in the WHERE clause + [] + let byFields tableName howMatched fields (patch: 'TPatch) = + use conn = Configuration.dbConn () + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Patch documents using a comparison on a JSON field in the WHERE clause [] + [] let byField tableName field (patch: 'TPatch) = - use conn = Configuration.dbConn () - WithConn.Patch.byField tableName field patch conn + byFields tableName Any [ field ] patch /// Commands to remove fields from documents [] module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames = use conn = Configuration.dbConn () WithConn.RemoveFields.byId tableName docId fieldNames conn - - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames) = + + /// Remove field from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames = use conn = Configuration.dbConn () - WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn) + WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn /// Remove field from documents via a comparison on a JSON field in the document - [] + [] + [] let byField tableName field fieldNames = - use conn = Configuration.dbConn () - WithConn.RemoveFields.byField tableName field fieldNames conn - - /// Remove field from documents via a comparison on a JSON field in the document - let ByField(tableName, field, fieldNames) = - use conn = Configuration.dbConn () - WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn) + byFields tableName Any [ field ] fieldNames /// Commands to delete documents [] @@ -772,9 +844,15 @@ module Delete = let byId tableName (docId: 'TKey) = use conn = Configuration.dbConn () WithConn.Delete.byId tableName docId conn - + + /// Delete documents by matching a comparison on JSON fields + [] + let byFields tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Delete.byFields tableName howMatched fields conn + /// Delete documents by matching a comparison on a JSON field [] + [] let byField tableName field = - use conn = Configuration.dbConn () - WithConn.Delete.byField tableName field conn + byFields tableName Any [ field ] diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 7bf446d..ddd9875 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -118,7 +118,6 @@ public static class PostgresCSharpTests Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); }) ]), -#pragma warning restore CS0618 TestList("FieldName", [ TestCase("succeeds for one name", () => @@ -140,6 +139,7 @@ public static class PostgresCSharpTests } }) ]) +#pragma warning restore CS0618 ]), TestList("Query", [ diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 3501695..87ceeea 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -18,10 +18,10 @@ public static class SqliteCSharpExtensionTests /// Integration tests for the SQLite extension methods /// [Tests] - public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[] - { - TestList("CustomSingle", new[] - { + public static readonly Test Integration = TestList("Sqlite.C#.Extensions", + [ + TestList("CustomSingle", + [ TestCase("succeeds when a row is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -43,9 +43,9 @@ public static class SqliteCSharpExtensionTests new[] { Parameters.Id("eighty") }, Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("CustomList", new[] - { + ]), + TestList("CustomList", + [ TestCase("succeeds when data is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -67,9 +67,9 @@ public static class SqliteCSharpExtensionTests new[] { new SqliteParameter("@value", 100) }, Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("CustomNonQuery", new[] - { + ]), + TestList("CustomNonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -93,7 +93,7 @@ public static class SqliteCSharpExtensionTests var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); }) - }), + ]), TestCase("CustomScalar succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -140,8 +140,8 @@ public static class SqliteCSharpExtensionTests exists = await indexExists(); Expect.isTrue(exists, "The index should now exist"); }), - TestList("Insert", new[] - { + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -168,9 +168,9 @@ public static class SqliteCSharpExtensionTests // This is what is supposed to happen } }) - }), - TestList("Save", new[] - { + ]), + TestList("Save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -203,7 +203,7 @@ public static class SqliteCSharpExtensionTests 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"); }) - }), + ]), TestCase("CountAll succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -213,6 +213,7 @@ public static class SqliteCSharpExtensionTests var theCount = await conn.CountAll(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), +#pragma warning disable CS0618 TestCase("CountByField succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -222,8 +223,9 @@ public static class SqliteCSharpExtensionTests var theCount = await conn.CountByField(SqliteDb.TableName, Field.EQ("Value", "purple")); Expect.equal(theCount, 2L, "There should have been 2 matching documents"); }), - TestList("ExistsById", new[] - { +#pragma warning restore CS0618 + TestList("ExistsById", + [ TestCase("succeeds when a document exists", async () => { await using var db = await SqliteDb.BuildDb(); @@ -242,9 +244,10 @@ public static class SqliteCSharpExtensionTests var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ExistsByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("ExistsByField", + [ TestCase("succeeds when documents exist", async () => { await using var db = await SqliteDb.BuildDb(); @@ -263,9 +266,10 @@ public static class SqliteCSharpExtensionTests var exists = await conn.ExistsByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("FindAll", new[] - { + ]), +#pragma warning restore CS0618 + TestList("FindAll", + [ TestCase("succeeds when there is data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -285,9 +289,9 @@ public static class SqliteCSharpExtensionTests var results = await conn.FindAll(SqliteDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("FindById", new[] - { + ]), + TestList("FindById", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -307,9 +311,10 @@ public static class SqliteCSharpExtensionTests var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("FindByField", + [ TestCase("succeeds when documents are found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -328,9 +333,9 @@ public static class SqliteCSharpExtensionTests var docs = await conn.FindByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindFirstByField", new[] - { + ]), + TestList("FindFirstByField", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -360,9 +365,10 @@ public static class SqliteCSharpExtensionTests var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("UpdateById", new[] - { + ]), +#pragma warning restore CS0618 + TestList("UpdateById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -389,9 +395,9 @@ public static class SqliteCSharpExtensionTests await conn.UpdateById(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("UpdateByFunc", new[] - { + ]), + TestList("UpdateByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -418,9 +424,9 @@ public static class SqliteCSharpExtensionTests await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - TestList("PatchById", new[] - { + ]), + TestList("PatchById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -443,9 +449,10 @@ public static class SqliteCSharpExtensionTests // This not raising an exception is the test await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("PatchByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("PatchByField", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -466,9 +473,10 @@ public static class SqliteCSharpExtensionTests // This not raising an exception is the test await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) - }), - TestList("RemoveFieldsById", new[] - { + ]), +#pragma warning restore CS0618 + TestList("RemoveFieldsById", + [ TestCase("succeeds when fields are removed", async () => { await using var db = await SqliteDb.BuildDb(); @@ -498,9 +506,10 @@ public static class SqliteCSharpExtensionTests // This not raising an exception is the test await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Value" }); }) - }), - TestList("RemoveFieldsByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("RemoveFieldsByField", + [ TestCase("succeeds when a field is removed", async () => { await using var db = await SqliteDb.BuildDb(); @@ -529,9 +538,10 @@ public static class SqliteCSharpExtensionTests // This not raising an exception is the test await conn.RemoveFieldsByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); }) - }), - TestList("DeleteById", new[] - { + ]), +#pragma warning restore CS0618 + TestList("DeleteById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -552,9 +562,10 @@ public static class SqliteCSharpExtensionTests var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByField", new[] - { + ]), +#pragma warning disable CS0618 + TestList("DeleteByField", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -575,7 +586,8 @@ public static class SqliteCSharpExtensionTests var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), + ]), +#pragma warning restore CS0618 TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + ]); } diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 3b9ac72..cac780c 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -63,6 +63,7 @@ public static class SqliteCSharpTests "WHERE clause not correct"); }) ]), +#pragma warning disable CS0618 TestList("WhereByField", [ TestCase("succeeds when a logical operator is passed", () => @@ -81,6 +82,7 @@ public static class SqliteCSharpTests "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); }) ]), +#pragma warning restore CS0618 TestCase("WhereById succeeds", () => { Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); @@ -102,12 +104,14 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Sqlite.Query.Count.ByField("tbl", Field.EQ("thatField", 0)), - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field", + "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0", "JSON field text comparison count query not correct"); }) +#pragma warning restore CS0618 ]), TestList("Exists", [ @@ -117,12 +121,14 @@ public static class SqliteCSharpTests "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Id' = @id) AS it", "ID existence query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Sqlite.Query.Exists.ByField("tbl", Field.LT("Test", 0)), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it", + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field0) AS it", "JSON field text comparison exists query not correct"); }) +#pragma warning restore CS0618 ]), TestList("Find", [ @@ -131,12 +137,14 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", "SELECT by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Sqlite.Query.Find.ByField("tbl", Field.GE("Golf", 0)), - "SELECT data FROM tbl WHERE data->>'Golf' >= @field", + "SELECT data FROM tbl WHERE data->>'Golf' >= @field0", "SELECT by JSON comparison query not correct"); }) +#pragma warning restore CS0618 ]), TestList("Patch", [ @@ -146,12 +154,14 @@ public static class SqliteCSharpTests "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id", "UPDATE partial by ID statement not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Sqlite.Query.Patch.ByField("tbl", Field.NE("Part", 0)), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field", + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field0", "UPDATE partial by JSON comparison query not correct"); }) +#pragma warning restore CS0618 ]), TestList("RemoveFields", [ @@ -161,13 +171,15 @@ public static class SqliteCSharpTests "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id", "Remove field by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0), new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }), - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field", + "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field0", "Remove field by field query not correct"); }) +#pragma warning restore CS0618 ]), TestList("Delete", [ @@ -176,11 +188,13 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", "DELETE by ID query not correct"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds", () => { 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"); }) +#pragma warning restore CS0618 ]) ]), TestList("Parameters", @@ -197,6 +211,7 @@ public static class SqliteCSharpTests Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect"); Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect"); }), +#pragma warning disable CS0618 TestCase("AddField succeeds when adding a parameter", () => { var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList(); @@ -210,6 +225,7 @@ public static class SqliteCSharpTests var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []); Expect.isEmpty(paramSeq, "There should not have been any parameters added"); }), +#pragma warning restore CS0618 TestCase("None succeeds", () => { Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); @@ -442,6 +458,7 @@ public static class SqliteCSharpTests var theCount = await Count.All(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), +#pragma warning disable CS0618 TestCase("ByField succeeds for numeric range", async () => { await using var db = await SqliteDb.BuildDb(); @@ -458,6 +475,7 @@ 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"); }) +#pragma warning restore CS0618 ]), TestList("Exists", [ @@ -480,6 +498,7 @@ public static class SqliteCSharpTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents exist", async () => @@ -499,6 +518,7 @@ public static class SqliteCSharpTests Expect.isFalse(exists, "There should not have been any existing documents"); }) ]) +#pragma warning restore CS0618 ]), TestList("Find", [ @@ -542,6 +562,7 @@ public static class SqliteCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents are found", async () => @@ -590,6 +611,7 @@ public static class SqliteCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]) +#pragma warning restore CS0618 ]), TestList("Update", [ @@ -676,6 +698,7 @@ public static class SqliteCSharpTests await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when a document is updated", async () => @@ -698,6 +721,7 @@ public static class SqliteCSharpTests await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); }) ]) +#pragma warning restore CS0618 ]), TestList("RemoveFields", [ @@ -730,6 +754,7 @@ public static class SqliteCSharpTests await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when a field is removed", async () => @@ -758,6 +783,7 @@ public static class SqliteCSharpTests await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); }) ]) +#pragma warning restore CS0618 ]), TestList("Delete", [ @@ -782,6 +808,7 @@ public static class SqliteCSharpTests Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]), +#pragma warning disable CS0618 TestList("ByField", [ TestCase("succeeds when documents are deleted", async () => @@ -803,6 +830,7 @@ public static class SqliteCSharpTests Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]) +#pragma warning restore CS0618 ]), TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) ]); diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 733d1d8..4fd7b75 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -94,7 +94,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Count.byField "tbl" (Field.EQ "thatField" 0)) - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field" + "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0" "JSON field text comparison count query not correct" } ] @@ -108,7 +108,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Exists.byField "tbl" (Field.LT "Test" 0)) - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it" + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field0) AS it" "JSON field text comparison exists query not correct" } ] @@ -122,7 +122,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Find.byField "tbl" (Field.GE "Golf" 0)) - "SELECT data FROM tbl WHERE data->>'Golf' >= @field" + "SELECT data FROM tbl WHERE data->>'Golf' >= @field0" "SELECT by JSON comparison query not correct" } ] @@ -136,7 +136,7 @@ let unitTests = test "byField succeeds" { Expect.equal (Query.Patch.byField "tbl" (Field.NE "Part" 0)) - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field" + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field0" "UPDATE partial by JSON comparison query not correct" } ] @@ -153,7 +153,7 @@ let unitTests = "tbl" (Field.GT "Fly" 0) [ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ]) - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' > @field" + "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' > @field0" "Remove field by field query not correct" } ] @@ -187,7 +187,7 @@ let unitTests = test "succeeds when adding a parameter" { let paramList = addFieldParam "@field" (Field.EQ "it" 99) [] Expect.hasLength paramList 1 "There should have been a parameter added" - let theParam = paramList[0] + let theParam = Seq.head paramList Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" Expect.equal theParam.Value 99 "The parameter value is incorrect" } -- 2.45.1 From 85750e19f277791a8838ac7d16d1f879307bd046 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 8 Aug 2024 22:36:48 -0400 Subject: [PATCH 08/29] Add Postgres Query byFields tests --- src/Tests.CSharp/PostgresCSharpTests.cs | 69 ++++++++++++++++++++++++ src/Tests/PostgresTests.fs | 71 ++++++++++++++++++++++--- src/Tests/SqliteExtensionTests.fs | 2 + 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index ddd9875..ed1b093 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -118,6 +118,29 @@ public static class PostgresCSharpTests Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); }) ]), +#pragma warning restore CS0618 + TestList("FieldNames", + [ + TestCase("succeeds for one name", () => + { + var (name, value) = Parameters.FieldNames(["bob"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsString) + { + Expect.isTrue(false, "The parameter was not a String type"); + } + }), + TestCase("succeeds for multiple names", () => + { + var (name, value) = Parameters.FieldNames(["bob", "tom", "mike"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsStringArray) + { + Expect.isTrue(false, "The parameter was not a StringArray type"); + } + }) + ]), +#pragma warning disable CS0618 TestList("FieldName", [ TestCase("succeeds for one name", () => @@ -269,6 +292,14 @@ public static class PostgresCSharpTests Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.Count.ByFields("x", FieldMatch.All, + [Field.EQ("thatField", 0), Field.EQ("anotherField", 8)]), + $"SELECT COUNT(*) AS it FROM x WHERE data->>'thatField' = @field0 AND data->>'anotherField' = @field1", + "JSON field text comparison count query not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { @@ -298,6 +329,14 @@ public static class PostgresCSharpTests $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it", "ID existence query not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.Exists.ByFields("q", FieldMatch.Any, + [Field.LT("Test", 0).WithParameterName("@a"), Field.LT("Unit", "x").WithParameterName("@b")]), + $"SELECT EXISTS (SELECT 1 FROM q WHERE data->>'Test' < @a OR data->>'Unit' < @b) AS it", + "JSON field text comparison exists query not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { @@ -327,6 +366,13 @@ public static class PostgresCSharpTests $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", "SELECT by ID query not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.Find.ByFields("x", FieldMatch.Any, [Field.GE("Golf", 0), Field.LE("Flog", 1)]), + $"SELECT data FROM x WHERE data->>'Golf' >= @field0 OR data->>'Flog' <= @field1", + "SELECT by JSON comparison query not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { @@ -356,6 +402,14 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id", "UPDATE partial by ID statement not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.Patch.ByFields("x", FieldMatch.All, + [Field.LT("Snail", 0), Field.BT("Slug", 8, 14)]), + $"UPDATE x SET data = data || @data WHERE data->>'Snail' < @field0 AND (data->>'Slug')::numeric BETWEEN @field1min AND @field1max", + "UPDATE partial by ID statement not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { @@ -385,6 +439,14 @@ public static class PostgresCSharpTests $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id", "Remove field by ID query not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.RemoveFields.ByFields("x", FieldMatch.Any, + [Field.LT("Fly", 0), Field.LT("Ant", 2)]), + $"UPDATE x SET data = data - @name WHERE data->>'Fly' < @field0 OR data->>'Ant' < @field1", + "Remove field by field query not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { @@ -414,6 +476,13 @@ public static class PostgresCSharpTests $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", "DELETE by ID query not correct"); }), + TestCase("ByFields succeeds", () => + { + Expect.equal( + Postgres.Query.Delete.ByFields("tbl", FieldMatch.All, [Field.NEX("gone"), Field.EX("here")]), + $"DELETE FROM tbl WHERE data->>'gone' IS NULL AND data->>'here' IS NOT NULL", + "DELETE by JSON comparison query not correct"); + }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 7683c42..229da1e 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -113,6 +113,23 @@ let unitTests = | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" } ] + testList "fieldNameParams" [ + test "succeeds for one name" { + let name, value = fieldNameParams [ "bob" ] + Expect.equal name "@name" "The parameter name was incorrect" + match value with + | SqlValue.String it -> Expect.equal it "bob" "The parameter value was incorrect" + | _ -> Expect.isTrue false "The parameter was not a String type" + } + test "succeeds for multiple names" { + let name, value = fieldNameParams [ "bob"; "tom"; "mike" ] + Expect.equal name "@name" "The parameter name was incorrect" + match value with + | SqlValue.StringArray it -> + Expect.equal it [| "bob"; "tom"; "mike" |] "The parameter value was incorrect" + | _ -> Expect.isTrue false "The parameter was not a StringArray type" + } + ] testList "fieldNameParam" [ test "succeeds for one name" { let name, value = fieldNameParam [ "bob" ] @@ -248,6 +265,12 @@ let unitTests = $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}" "Count query not correct" } + test "byFields succeeds" { + Expect.equal + (Query.Count.byFields "tbl" All [ Field.EQ "thatField" 0; Field.EQ "anotherField" 8]) + $"SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0 AND data->>'anotherField' = @field1" + "JSON field text comparison count query not correct" + } test "byField succeeds" { Expect.equal (Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0)) @@ -274,6 +297,14 @@ let unitTests = $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it" "ID existence query not correct" } + test "byFields succeeds" { + Expect.equal + (Query.Exists.byFields "tbl" Any + [ { Field.LT "Test" 0 with ParameterName = Some "@a" } + { Field.GT "Unit" "x" with ParameterName = Some "@b" } ]) + $"SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @a OR data->>'Unit' > @b) AS it" + "JSON field text comparison exists query not correct" + } test "byField succeeds" { Expect.equal (Query.Exists.byField PostgresDb.TableName (Field.LT "Test" 0)) @@ -300,6 +331,12 @@ let unitTests = $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" "SELECT by ID query not correct" } + test "byFields succeeds" { + Expect.equal + (Query.Find.byFields "tbl" Any [ Field.GE "Golf" 0; Field.LE "Flog" 1 ]) + $"SELECT data FROM tbl WHERE data->>'Golf' >= @field0 OR data->>'Flog' <= @field1" + "SELECT by JSON comparison query not correct" + } test "byField succeeds" { Expect.equal (Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0)) @@ -326,6 +363,12 @@ let unitTests = $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id" "UPDATE partial by ID statement not correct" } + test "byFields succeeds" { + Expect.equal + (Query.Patch.byFields "x" All [ Field.LT "Snail" 0; Field.BT "Slug" 8 12 ]) + $"UPDATE x SET data = data || @data WHERE data->>'Snail' < @field0 AND (data->>'Slug')::numeric BETWEEN @field1min AND @field1max" + "UPDATE partial by ID statement not correct" + } test "byField succeeds" { Expect.equal (Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) @@ -348,26 +391,32 @@ let unitTests = testList "RemoveFields" [ test "byId succeeds" { Expect.equal - (Query.RemoveFields.byId "tbl") - "UPDATE tbl SET data = data - @name WHERE data->>'Id' = @id" + (Query.RemoveFields.byId PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id" "Remove field by ID query not correct" } + test "byFields succeeds" { + Expect.equal + (Query.RemoveFields.byFields "tbl" Any [ Field.LT "Fly" 0; Field.LT "Ant" 2 ]) + "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field0 OR data->>'Ant' < @field1" + "Remove field by field query not correct" + } test "byField succeeds" { Expect.equal - (Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0)) - "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field0" + (Query.RemoveFields.byField PostgresDb.TableName (Field.LT "Fly" 0)) + $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field0" "Remove field by field query not correct" } test "byContains succeeds" { Expect.equal - (Query.RemoveFields.byContains "tbl") - "UPDATE tbl SET data = data - @name WHERE data @> @criteria" + (Query.RemoveFields.byContains PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @> @criteria" "Remove field by contains query not correct" } test "byJsonPath succeeds" { Expect.equal - (Query.RemoveFields.byJsonPath "tbl") - "UPDATE tbl SET data = data - @name WHERE data @? @path::jsonpath" + (Query.RemoveFields.byJsonPath PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath" "Remove field by JSON path query not correct" } ] @@ -378,6 +427,12 @@ let unitTests = $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" "DELETE by ID query not correct" } + test "byFields succeeds" { + Expect.equal + (Query.Delete.byFields PostgresDb.TableName All [ Field.NEX "gone"; Field.EX "here" ]) + $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL AND data->>'here' IS NOT NULL" + "DELETE by JSON comparison query not correct" + } test "byField succeeds" { Expect.equal (Query.Delete.byField PostgresDb.TableName (Field.NEX "gone")) diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index d8a43ff..9fd5bad 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -8,6 +8,8 @@ open Expecto open Microsoft.Data.Sqlite open Types +#nowarn "0044" + /// Integration tests for the F# extensions on the SqliteConnection data type let integrationTests = let loadDocs () = backgroundTask { -- 2.45.1 From b1c3991e11dce224e7f5bc49dd6be5c3b1822b09 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 9 Aug 2024 18:42:10 -0400 Subject: [PATCH 09/29] WIP on supporting numeric fields --- src/Postgres/Library.fs | 18 ++++---- src/Tests/PostgresTests.fs | 86 +++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 4be1a0c..d7d66f2 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -121,15 +121,17 @@ module Query = |> Seq.map (fun it -> match it.Op with | EX | NEX -> $"{it.PgSqlPath} {it.Op}" - | BT -> - let p = name.Derive it.ParameterName - let names = $"{p}min AND {p}max" - let values = it.Value :?> obj list - match values[0] with + | _ -> + let p = name.Derive it.ParameterName + let path, value = + match it.Op with + | BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0] + | _ -> p, it.Value + printfn $"%A{value}" + match value with | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"({it.PgSqlPath})::numeric {it.Op} {names}" - | _ -> $"{it.PgSqlPath} {it.Op} {names}" - | _ -> $"{it.PgSqlPath} {it.Op} {name.Derive it.ParameterName}") + | :? decimal | :? single | :? double -> $"({it.PgSqlPath})::numeric {it.Op} {path}" + | _ -> $"{it.PgSqlPath} {it.Op} {path}") |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 229da1e..bbb9bef 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -664,20 +664,39 @@ let integrationTests = let! theCount = Count.all PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - testTask "byField succeeds for numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20) - Expect.equal theCount 3 "There should have been 3 matching documents" - } - testTask "byField succeeds for non-numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "Value" "aardvark" "apple") - Expect.equal theCount 1 "There should have been 1 matching document" - } + testList "byFields" [ + testTask "succeeds when items are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = + Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ] + Expect.equal theCount 3 "There should have been 3 matching documents" + } + testTask "succeeds when items are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ] + Expect.equal theCount 0 "There should have been no matching documents" + } + ] + testList "byField" [ + testTask "succeeds for numeric range" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20) + Expect.equal theCount 3 "There should have been 3 matching documents" + } + testTask "succeeds for non-numeric range" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byField PostgresDb.TableName (Field.BT "Value" "aardvark" "apple") + Expect.equal theCount 1 "There should have been 1 matching document" + } + ] testTask "byContains succeeds" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -710,6 +729,23 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] + testList "byFields" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ] + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when documents do not exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = + Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ] + Expect.isFalse exists "There should not have been existing documents" + } + ] testList "byField" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() @@ -782,7 +818,7 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] - testList "byId" [ + ftestList "byId" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -799,6 +835,26 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] + testList "byFields" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] + Expect.equal (List.length docs) 1 "There should have been one document returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ] + Expect.isEmpty docs "There should have been no documents returned" + } + ] testList "byField" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() -- 2.45.1 From 35755df99aa3cebea82ff027c72fd05ba63cb28a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 9 Aug 2024 20:15:08 -0400 Subject: [PATCH 10/29] WIP on pulling code up to Common --- src/Common/Library.fs | 52 +++++++++++++------ src/Postgres/Library.fs | 25 ++++----- src/Sqlite/Library.fs | 17 +++---- src/Tests.CSharp/CommonCSharpTests.cs | 68 +++++++++++++++---------- src/Tests.CSharp/PostgresCSharpTests.cs | 5 -- src/Tests.CSharp/PostgresDb.cs | 3 +- src/Tests.CSharp/SqliteCSharpTests.cs | 5 -- src/Tests/CommonTests.fs | 64 +++++++++++++---------- src/Tests/PostgresTests.fs | 12 ++--- src/Tests/SqliteTests.fs | 3 -- 10 files changed, 135 insertions(+), 119 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 564132e..3ae755a 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -35,6 +35,12 @@ type Op = | NEX -> "IS NULL" +/// The dialect in which a command should be rendered +[] +type Dialect = + | PostgreSQL + | SQLite + /// Criteria for a field WHERE clause type Field = { /// The name of the field @@ -89,6 +95,16 @@ type Field = { static member NEX name = { Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None } + /// Transform a field name (a.b.c) to a path for the given SQL dialect + static member NameToPath (name: string) dialect = + let path = + if name.Contains '.' then + match dialect with + | PostgreSQL -> "#>>'{" + String.concat "," (name.Split '.') + "}'" + | SQLite -> "->>'" + String.concat "'->>'" (name.Split '.') + "'" + else $"->>'{name}'" + $"data{path}" + /// Specify the name of the parameter for this field member this.WithParameterName name = { this with ParameterName = Some name } @@ -97,17 +113,9 @@ 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}'" + /// Get the qualified path to the field + member this.Path dialect = + (this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect /// How fields should be matched @@ -221,7 +229,7 @@ module Query = /// SQL statement to create an index on one or more fields in a JSON document [] - let ensureIndexOn tableName indexName (fields: string seq) = + let ensureIndexOn tableName indexName (fields: string seq) dialect = let _, tbl = splitSchemaAndTable tableName let jsonFields = fields @@ -229,14 +237,14 @@ module Query = let parts = it.Split ' ' let fieldName = if Array.length parts = 1 then it else parts[0] let direction = if Array.length parts < 2 then "" else $" {parts[1]}" - $"(data ->> '{fieldName}'){direction}") + $"({Field.NameToPath fieldName dialect}){direction}") |> String.concat ", " $"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})" /// SQL statement to create a key index for a document table [] - let ensureKey tableName = - (ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX") + let ensureKey tableName dialect = + (ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX") /// Query to insert a document [] @@ -249,4 +257,16 @@ module Query = sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - \ No newline at end of file + + /// Queries for counting documents + module Count = + + /// Query to count all documents in a table + [] + let all tableName = + $"SELECT COUNT(*) AS it FROM %s{tableName}" + + /// Query to count matching documents using a text comparison on JSON fields + [] + let byFields (whereByFields: FieldMatch -> Field seq -> string) tableName howMatched fields = + $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index d7d66f2..9138811 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -120,18 +120,18 @@ module Query = fields |> Seq.map (fun it -> match it.Op with - | EX | NEX -> $"{it.PgSqlPath} {it.Op}" - | _ -> + | EX | NEX -> $"{it.Path PostgreSQL} {it.Op}" + | BT -> let p = name.Derive it.ParameterName let path, value = match it.Op with | BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0] | _ -> p, it.Value - printfn $"%A{value}" match value with | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"({it.PgSqlPath})::numeric {it.Op} {path}" - | _ -> $"{it.PgSqlPath} {it.Op} {path}") + | :? decimal | :? single | :? double -> $"({it.Path PostgreSQL})::numeric {it.Op} {path}" + | _ -> $"{it.Path PostgreSQL} {it.Op} {path}" + | _ -> $"{it.Path PostgreSQL} {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 a field in a JSON document @@ -178,15 +178,10 @@ module Query = /// Queries for counting documents module Count = - /// Query to count all documents in a table - [] - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - /// Query to count matching documents using a text comparison on JSON fields [] let byFields tableName howMatched fields = - $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" + Query.Count.byFields whereByFields tableName howMatched fields /// Query to count matching documents using a text comparison on a JSON field [] @@ -197,12 +192,12 @@ module Query = /// Query to count matching documents using a JSON containment query (@>) [] let byContains tableName = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" + $"""{Query.Count.all tableName} WHERE {whereDataContains "@criteria"}""" /// Query to count matching documents using a JSON Path match (@?) [] let byJsonPath tableName = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" + $"""{Query.Count.all tableName} WHERE {whereJsonPathMatches "@path"}""" /// Queries for determining document existence module Exists = @@ -445,7 +440,7 @@ module WithProps = [] let ensureTable name sqlProps = backgroundTask { do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps - do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps + do! Custom.nonQuery (Query.Definition.ensureKey name PostgreSQL) [] sqlProps } /// Create an index on documents in the specified table @@ -456,7 +451,7 @@ module WithProps = /// Create an index on field(s) within documents in the specified table [] let ensureFieldIndex tableName indexName fields sqlProps = - Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] sqlProps + Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields PostgreSQL) [] sqlProps /// Commands to add documents [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 86df67b..b716fa0 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -38,11 +38,11 @@ module Query = fields |> Seq.map (fun it -> match it.Op with - | EX | NEX -> $"{it.SqlitePath} {it.Op}" + | EX | NEX -> $"{it.Path SQLite} {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}") + $"{it.Path SQLite} {it.Op} {p}min AND {p}max" + | _ -> $"{it.Path SQLite} {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 a field in a JSON document @@ -72,15 +72,10 @@ module Query = /// Queries for counting documents module Count = - /// Query to count all documents in a table - [] - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - /// Query to count matching documents using a text comparison on JSON fields [] let byFields tableName howMatched fields = - $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" + Query.Count.byFields whereByFields tableName howMatched fields /// Query to count matching documents using a text comparison on a JSON field [] @@ -361,13 +356,13 @@ module WithConn = [] let ensureTable name conn = backgroundTask { do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn - do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn + do! Custom.nonQuery (Query.Definition.ensureKey name SQLite) [] conn } /// Create an index on a document table [] let ensureFieldIndex tableName indexName fields conn = - Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn + Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn /// Insert a new document [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index eca9cfd..e823508 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -185,50 +185,54 @@ public static class CommonCSharpTests Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); }), - TestList("PgSqlPath", + TestList("Path", [ - TestCase("succeeds for a single field with no qualifier", () => + TestCase("succeeds for a PostgreSQL single field with no qualifier", () => { var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.PgSqlPath, "The PostgreSQL path is incorrect"); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); }), - TestCase("succeeds for a single field with a qualifier", () => + TestCase("succeeds for a PostgreSQL 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"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); }), - TestCase("succeeds for a nested field with no qualifier", () => + TestCase("succeeds for a PostgreSQL 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"); + Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); }), - TestCase("succeeds for a nested field with a qualifier", () => + TestCase("succeeds for a PostgreSQL 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", () => + Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with no qualifier", () => { var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.SqlitePath, "The SQLite path is incorrect"); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); }), - TestCase("succeeds for a single field with a qualifier", () => + TestCase("succeeds for a SQLite 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"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); }), - TestCase("succeeds for a nested field with no qualifier", () => + TestCase("succeeds for a SQLite 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"); + Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); }), - TestCase("succeeds for a nested field with a qualifier", () => + TestCase("succeeds for a SQLite 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"); + Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); }) ]) ]), @@ -251,14 +255,14 @@ public static class CommonCSharpTests [ TestCase("succeeds when a schema is present", () => { - Expect.equal(Query.Definition.EnsureKey("test.table"), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))", + Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", "CREATE INDEX for key statement with schema not constructed correctly"); }), TestCase("succeeds when a schema is not present", () => { - Expect.equal(Query.Definition.EnsureKey("table"), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))", + Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", "CREATE INDEX for key statement without schema not constructed correctly"); }) ]), @@ -266,9 +270,9 @@ public static class CommonCSharpTests { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", - new[] { "taco", "guac DESC", "salsa ASC" }), + new[] { "taco", "guac DESC", "salsa ASC" }, Dialect.SQLite), "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", "CREATE INDEX for multiple field statement incorrect"); }) ]), @@ -281,7 +285,15 @@ public static class CommonCSharpTests Expect.equal(Query.Save("tbl"), "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); - }) + }), + TestList("Count", + [ + TestCase("All succeeds", () => + { + Expect.equal(Query.Count.All("a_table"), $"SELECT COUNT(*) AS it FROM a_table", + "Count query not correct"); + }), + ]) ]) ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index ed1b093..d3c239b 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -287,11 +287,6 @@ public static class PostgresCSharpTests }), TestList("Count", [ - TestCase("All succeeds", () => - { - Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct"); - }), TestCase("ByFields succeeds", () => { Expect.equal( diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs index 301e9a6..4d84e2c 100644 --- a/src/Tests.CSharp/PostgresDb.cs +++ b/src/Tests.CSharp/PostgresDb.cs @@ -1,3 +1,4 @@ +using BitBadger.Documents.Postgres; using Npgsql; using Npgsql.FSharp; using ThrowawayDb.Postgres; @@ -131,7 +132,7 @@ public static class PostgresDb var sqlProps = Sql.connect(database.ConnectionString); Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps)); - Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps)); + Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName, Dialect.PostgreSQL), sqlProps)); Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString)); diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index cac780c..ca57e8b 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -99,11 +99,6 @@ public static class SqliteCSharpTests }), TestList("Count", [ - TestCase("All succeeds", () => - { - Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", - "Count query not correct"); - }), #pragma warning disable CS0618 TestCase("ByField succeeds", () => { diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 27d6fff..5020d26 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -119,40 +119,39 @@ 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" { + testList "Path" [ + test "succeeds for a PostgreSQL single field with no qualifier" { let field = Field.GE "SomethingCool" 18 - Expect.equal "data->>'SomethingCool'" field.PgSqlPath "The PostgreSQL path is incorrect" + Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" } - test "succeeds for a single field with a qualifier" { + test "succeeds for a PostgreSQL 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" + Expect.equal + "this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" } - test "succeeds for a nested field with no qualifier" { + test "succeeds for a PostgreSQL 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" + Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" } - test "succeeds for a nested field with a qualifier" { + test "succeeds for a PostgreSQL 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" + Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" } - ] - testList "SqlitePath" [ - test "succeeds for a single field with no qualifier" { + test "succeeds for a SQLite single field with no qualifier" { let field = Field.GE "SomethingCool" 18 - Expect.equal "data->>'SomethingCool'" field.SqlitePath "The SQLite path is incorrect" + Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect" } - test "succeeds for a single field with a qualifier" { + test "succeeds for a SQLite 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" + Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect" } - test "succeeds for a nested field with no qualifier" { + test "succeeds for a SQLite 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" + Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect" } - test "succeeds for a nested field with a qualifier" { + test "succeeds for a SQLite 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" + Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect" } ] ] @@ -184,22 +183,23 @@ let all = testList "ensureKey" [ test "succeeds when a schema is present" { Expect.equal - (Query.Definition.ensureKey "test.table") - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))" + (Query.Definition.ensureKey "test.table" PostgreSQL) + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))" "CREATE INDEX for key statement with schema not constructed correctly" } test "succeeds when a schema is not present" { Expect.equal - (Query.Definition.ensureKey "table") - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))" + (Query.Definition.ensureKey "table" SQLite) + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))" "CREATE INDEX for key statement without schema not constructed correctly" } ] test "ensureIndexOn succeeds for multiple fields and directions" { Expect.equal - (Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ]) + (Query.Definition.ensureIndexOn + "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)" ] + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] |> String.concat "") "CREATE INDEX for multiple field statement incorrect" } @@ -213,6 +213,18 @@ let all = $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" "INSERT ON CONFLICT UPDATE statement not correct" } + testList "Count" [ + test "all succeeds" { + Expect.equal (Query.Count.all "a_table") "SELECT COUNT(*) AS it FROM a_table" + "Count query not correct" + } + test "byFields succeeds" { + let test = fun _ _ -> "howdy" + Expect.equal + (Query.Count.byFields test "over_here" Any []) + "SELECT COUNT(*) AS it FROM over_here WHERE howdy" + "Count by fields query not correct" + } + ] ] ] - diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index bbb9bef..81d5052 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -259,12 +259,6 @@ let unitTests = Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" } testList "Count" [ - test "all succeeds" { - Expect.equal - (Query.Count.all PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}" - "Count query not correct" - } test "byFields succeeds" { Expect.equal (Query.Count.byFields "tbl" All [ Field.EQ "thatField" 0; Field.EQ "anotherField" 8]) @@ -664,7 +658,7 @@ let integrationTests = let! theCount = Count.all PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - testList "byFields" [ + ptestList "byFields" [ testTask "succeeds when items are found" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -818,7 +812,7 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] - ftestList "byId" [ + testList "byId" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -845,7 +839,7 @@ let integrationTests = PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] Expect.equal (List.length docs) 1 "There should have been one document returned" } - testTask "succeeds when documents are not found" { + ptestTask "succeeds when documents are not found" { use db = PostgresDb.BuildDb() do! loadDocs () diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 4fd7b75..709f50a 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -88,9 +88,6 @@ let unitTests = "UPDATE full statement not correct" } testList "Count" [ - test "all succeeds" { - Expect.equal (Query.Count.all "tbl") $"SELECT COUNT(*) AS it FROM tbl" "Count query not correct" - } test "byField succeeds" { Expect.equal (Query.Count.byField "tbl" (Field.EQ "thatField" 0)) -- 2.45.1 From 98bc83ac1768bd5f2e2654cb90210e54d35714ea Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 9 Aug 2024 22:55:12 -0400 Subject: [PATCH 11/29] Pull count, exists, update to common --- src/Common/Library.fs | 31 ++++--- src/Postgres/Library.fs | 113 ++++++++---------------- src/Sqlite/Library.fs | 71 ++++----------- src/Tests.CSharp/CommonCSharpTests.cs | 10 +-- src/Tests.CSharp/PostgresCSharpTests.cs | 73 --------------- src/Tests.CSharp/SqliteCSharpTests.cs | 33 ------- src/Tests/CommonTests.fs | 16 +--- src/Tests/PostgresTests.fs | 66 -------------- src/Tests/SqliteTests.fs | 28 ------ 9 files changed, 75 insertions(+), 366 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 3ae755a..a1cc7a2 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -209,6 +209,11 @@ module Configuration = [] module Query = + /// Combine a query (select, update, etc.) and a WHERE clause + [] + let statementWhere statement where = + $"%s{statement} WHERE %s{where}" + /// Create a SELECT clause to retrieve the document data from the given table [] let selectFromTable tableName = @@ -258,15 +263,17 @@ module Query = "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - /// Queries for counting documents - module Count = - - /// Query to count all documents in a table - [] - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - - /// Query to count matching documents using a text comparison on JSON fields - [] - let byFields (whereByFields: FieldMatch -> Field seq -> string) tableName howMatched fields = - $"SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByFields howMatched fields}" + /// Query to update a document (no WHERE clause) + [] + let update tableName = + $"UPDATE %s{tableName} SET data = @data" + + /// Query to count documents in a table (no WHERE clause) + [] + let count tableName = + $"SELECT COUNT(*) AS it FROM %s{tableName}" + + /// Query to check for document existence in a table + [] + let exists tableName where = + $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it" diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 9138811..69f86e0 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -160,11 +160,6 @@ module Query = let tableName = name.Split '.' |> Array.last $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" - /// Query to update a document - [] - let update tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Create a WHERE clause fragment to implement a @> (JSON contains) condition [] let whereDataContains paramName = @@ -175,58 +170,17 @@ module Query = let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" - /// Queries for counting documents - module Count = - - /// Query to count matching documents using a text comparison on JSON fields - [] - let byFields tableName howMatched fields = - Query.Count.byFields whereByFields tableName howMatched fields - - /// Query to count matching documents using a text comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to count matching documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""{Query.Count.all tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to count matching documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""{Query.Count.all tableName} WHERE {whereJsonPathMatches "@path"}""" + /// Create a query on JSON fields + let fieldQuery statement howMatched fields = + Query.statementWhere statement (whereByFields howMatched fields) - /// Queries for determining document existence - module Exists = - - /// Query to determine if a document exists for the given ID - [] - let byId tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" - - /// Query to determine if documents exist using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByFields howMatched fields}) AS it" - - /// Query to determine if documents exist using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to determine if documents exist using a JSON containment query (@>) - [] - let byContains tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it""" - - /// Query to determine if documents exist using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}) AS it""" + /// Create a JSON containment query + let containQuery statement = + Query.statementWhere statement (whereDataContains "@criteria") + + /// Create a JSON Path match query + let pathMatchQuery statement = + Query.statementWhere statement (whereJsonPathMatches "@path") /// Queries for retrieving documents module Find = @@ -474,28 +428,25 @@ module WithProps = /// Count all documents in a table [] let all tableName sqlProps = - Custom.scalar (Query.Count.all tableName) [] toCount sqlProps + Custom.scalar (Query.count tableName) [] toCount sqlProps /// Count matching documents using JSON field comparisons (->> =) [] let byFields tableName howMatched fields sqlProps = - Custom.scalar (Query.Count.byFields tableName howMatched fields) (addFieldParams fields []) toCount sqlProps - - /// Count matching documents using a JSON field comparison (->> =) - [] - [] - let byField tableName field sqlProps = - byFields tableName Any [ field ] sqlProps + Custom.scalar + (Query.fieldQuery (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount sqlProps /// Count matching documents using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = - Custom.scalar (Query.Count.byContains tableName) [ jsonParam "@criteria" criteria ] toCount sqlProps + Custom.scalar + (Query.containQuery (Query.count tableName)) [ jsonParam "@criteria" criteria ] toCount sqlProps /// Count matching documents using a JSON Path match query (@?) [] let byJsonPath tableName jsonPath sqlProps = - Custom.scalar (Query.Count.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toCount sqlProps + Custom.scalar + (Query.pathMatchQuery (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps /// Commands to determine if documents exist [] @@ -504,29 +455,34 @@ module WithProps = /// Determine if a document exists for the given ID [] let byId tableName (docId: 'TKey) sqlProps = - Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps + Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists sqlProps /// Determine if a document exists using JSON field comparisons (->> =) [] let byFields tableName howMatched fields sqlProps = Custom.scalar - (Query.Exists.byFields tableName howMatched fields) (addFieldParams fields []) toExists sqlProps + (Query.exists tableName (Query.whereByFields howMatched fields)) + (addFieldParams fields []) + toExists + sqlProps - /// Determine if a document exists using a JSON field comparison (->> =) - [] - [] - let byField tableName field sqlProps = - byFields tableName Any [ field ] sqlProps - /// Determine if a document exists using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = - Custom.scalar (Query.Exists.byContains tableName) [ jsonParam "@criteria" criteria ] toExists sqlProps + Custom.scalar + (Query.exists tableName (Query.whereDataContains "@criteria")) + [ jsonParam "@criteria" criteria ] + toExists + sqlProps /// Determine if a document exists using a JSON Path match query (@?) [] let byJsonPath tableName jsonPath sqlProps = - Custom.scalar (Query.Exists.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toExists sqlProps + Custom.scalar + (Query.exists tableName (Query.whereJsonPathMatches "@path")) + [ "@path", Sql.string jsonPath ] + toExists + sqlProps /// Commands to determine if documents exist [] @@ -657,7 +613,10 @@ module WithProps = /// Update an entire document by its ID [] let byId tableName (docId: 'TKey) (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] sqlProps + Custom.nonQuery + (Query.statementWhere (Query.update tableName) (Query.whereById "@id")) + [ idParam docId; jsonParam "@data" document ] + sqlProps /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index b716fa0..a0d861a 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -64,44 +64,6 @@ module Query = let ensureTable name = Query.Definition.ensureTableFor name "TEXT" - /// Query to update a document - [] - let update tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - - /// Queries for counting documents - module Count = - - /// Query to count matching documents using a text comparison on JSON fields - [] - let byFields tableName howMatched fields = - Query.Count.byFields whereByFields tableName howMatched fields - - /// Query to count matching documents using a text comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Queries for determining document existence - module Exists = - - /// Query to determine if a document exists for the given ID - [] - let byId tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" - - /// Query to determine if documents exist using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByFields howMatched fields}) AS it" - - /// Query to determine if documents exist using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - /// Queries for retrieving documents module Find = @@ -381,18 +343,16 @@ module WithConn = /// Count all documents in a table [] let all tableName conn = - Custom.scalar (Query.Count.all tableName) [] toCount conn + Custom.scalar (Query.count tableName) [] toCount conn /// Count matching documents using a comparison on JSON fields [] let byFields tableName howMatched fields conn = - Custom.scalar (Query.Count.byFields tableName howMatched fields) (addFieldParams fields []) toCount conn - - /// Count matching documents using a comparison on a JSON field - [] - [] - let byField tableName field conn = - byFields tableName Any [ field ] conn + Custom.scalar + (Query.statementWhere (Query.count tableName) (Query.whereByFields howMatched fields)) + (addFieldParams fields []) + toCount + conn /// Commands to determine if documents exist [] @@ -401,18 +361,16 @@ module WithConn = /// Determine if a document exists for the given ID [] let byId tableName (docId: 'TKey) conn = - Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists conn + Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists conn /// Determine if a document exists using a comparison on JSON fields [] let byFields tableName howMatched fields conn = - Custom.scalar (Query.Exists.byFields tableName howMatched fields) (addFieldParams fields []) toExists conn - - /// Determine if a document exists using a comparison on a JSON field - [] - [] - let byField tableName field conn = - byFields tableName Any [ field ] conn + Custom.scalar + (Query.exists tableName (Query.whereByFields howMatched fields)) + (addFieldParams fields []) + toExists + conn /// Commands to retrieve documents [] @@ -493,7 +451,10 @@ module WithConn = /// Update an entire document by its ID [] let byId tableName (docId: 'TKey) (document: 'TDoc) conn = - Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn + Custom.nonQuery + (Query.statementWhere (Query.update tableName) (Query.whereById "@id")) + [ idParam docId; jsonParam "@data" document ] + conn /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index e823508..d1ed8cd 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -285,15 +285,7 @@ public static class CommonCSharpTests Expect.equal(Query.Save("tbl"), "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); - }), - TestList("Count", - [ - TestCase("All succeeds", () => - { - Expect.equal(Query.Count.All("a_table"), $"SELECT COUNT(*) AS it FROM a_table", - "Count query not correct"); - }), - ]) + }) ]) ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index d3c239b..cc115fd 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -270,11 +270,6 @@ public static class PostgresCSharpTests "CREATE INDEX statement not constructed correctly"); }) ]), - TestCase("Update succeeds", () => - { - Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", - "UPDATE full statement not correct"); - }), TestCase("WhereDataContains succeeds", () => { Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", @@ -285,74 +280,6 @@ public static class PostgresCSharpTests Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", "WHERE clause not correct"); }), - TestList("Count", - [ - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.Count.ByFields("x", FieldMatch.All, - [Field.EQ("thatField", 0), Field.EQ("anotherField", 8)]), - $"SELECT COUNT(*) AS it FROM x WHERE data->>'thatField' = @field0 AND data->>'anotherField' = @field1", - "JSON field text comparison count query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByField(PostgresDb.TableName, Field.EQ("thatField", 0)), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field0", - "JSON field text comparison count query not correct"); - }), -#pragma warning restore CS0618 - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria", - "JSON containment count query not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByJsonPath(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "JSON Path match count query not correct"); - }) - ]), - TestList("Exists", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it", - "ID existence query not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.Exists.ByFields("q", FieldMatch.Any, - [Field.LT("Test", 0).WithParameterName("@a"), Field.LT("Unit", "x").WithParameterName("@b")]), - $"SELECT EXISTS (SELECT 1 FROM q WHERE data->>'Test' < @a OR data->>'Unit' < @b) AS it", - "JSON field text comparison exists query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByField(PostgresDb.TableName, Field.LT("Test", 0)), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field0) AS it", - "JSON field text comparison exists query not correct"); - }), -#pragma warning restore CS0618 - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it", - "JSON containment exists query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByJsonPath(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it", - "JSON Path match existence query not correct"); - }) - ]), TestList("Find", [ TestCase("ById succeeds", () => diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index ca57e8b..be07d87 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -92,39 +92,6 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); }), - TestCase("Update succeeds", () => - { - Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", - "UPDATE full statement not correct"); - }), - TestList("Count", - [ -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Count.ByField("tbl", Field.EQ("thatField", 0)), - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0", - "JSON field text comparison count query not correct"); - }) -#pragma warning restore CS0618 - ]), - TestList("Exists", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Exists.ById("tbl"), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Id' = @id) AS it", - "ID existence query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Exists.ByField("tbl", Field.LT("Test", 0)), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field0) AS it", - "JSON field text comparison exists query not correct"); - }) -#pragma warning restore CS0618 - ]), TestList("Find", [ TestCase("ById succeeds", () => diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 5020d26..4e2eab0 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -213,18 +213,8 @@ let all = $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" "INSERT ON CONFLICT UPDATE statement not correct" } - testList "Count" [ - test "all succeeds" { - Expect.equal (Query.Count.all "a_table") "SELECT COUNT(*) AS it FROM a_table" - "Count query not correct" - } - test "byFields succeeds" { - let test = fun _ _ -> "howdy" - Expect.equal - (Query.Count.byFields test "over_here" Any []) - "SELECT COUNT(*) AS it FROM over_here WHERE howdy" - "Count by fields query not correct" - } - ] + test "count succeeds" { + Expect.equal (Query.count "a_table") "SELECT COUNT(*) AS it FROM a_table" "Count query not correct" + } ] ] diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 81d5052..d217a2b 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -246,78 +246,12 @@ let unitTests = "CREATE INDEX statement not constructed correctly" } ] - test "update succeeds" { - Expect.equal - (Query.update PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = @data WHERE data->>'Id' = @id" - "UPDATE full statement not correct" - } test "whereDataContains succeeds" { Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" } test "whereJsonPathMatches succeeds" { Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" } - testList "Count" [ - test "byFields succeeds" { - Expect.equal - (Query.Count.byFields "tbl" All [ Field.EQ "thatField" 0; Field.EQ "anotherField" 8]) - $"SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0 AND data->>'anotherField' = @field1" - "JSON field text comparison count query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0)) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field0" - "JSON field text comparison count query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Count.byContains PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria" - "JSON containment count query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Count.byJsonPath PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "JSON Path match count query not correct" - } - ] - testList "Exists" [ - test "byId succeeds" { - Expect.equal - (Query.Exists.byId PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it" - "ID existence query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.Exists.byFields "tbl" Any - [ { Field.LT "Test" 0 with ParameterName = Some "@a" } - { Field.GT "Unit" "x" with ParameterName = Some "@b" } ]) - $"SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @a OR data->>'Unit' > @b) AS it" - "JSON field text comparison exists query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Exists.byField PostgresDb.TableName (Field.LT "Test" 0)) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field0) AS it" - "JSON field text comparison exists query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Exists.byContains PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it" - "JSON containment exists query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Exists.byJsonPath PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it" - "JSON Path match existence query not correct" - } - ] testList "Find" [ test "byId succeeds" { Expect.equal diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 709f50a..a916473 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -81,34 +81,6 @@ let unitTests = "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" "CREATE TABLE statement not correct" } - test "update succeeds" { - Expect.equal - (Query.update "tbl") - "UPDATE tbl SET data = @data WHERE data->>'Id' = @id" - "UPDATE full statement not correct" - } - testList "Count" [ - test "byField succeeds" { - Expect.equal - (Query.Count.byField "tbl" (Field.EQ "thatField" 0)) - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field0" - "JSON field text comparison count query not correct" - } - ] - testList "Exists" [ - test "byId succeeds" { - Expect.equal - (Query.Exists.byId "tbl") - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Id' = @id) AS it" - "ID existence query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Exists.byField "tbl" (Field.LT "Test" 0)) - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field0) AS it" - "JSON field text comparison exists query not correct" - } - ] testList "Find" [ test "byId succeeds" { Expect.equal -- 2.45.1 From 74e5b77edbe45e71c28f17f3baf9e3589edf8d70 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 10 Aug 2024 09:56:59 -0400 Subject: [PATCH 12/29] Complete common migration, WIP on tests --- src/Common/Library.fs | 31 ++- src/Postgres/Extensions.fs | 6 +- src/Postgres/Library.fs | 281 ++++++++---------------- src/Sqlite/Library.fs | 156 +++++-------- src/Tests.CSharp/CommonCSharpTests.cs | 61 ++++- src/Tests.CSharp/PostgresCSharpTests.cs | 148 +------------ src/Tests.CSharp/SqliteCSharpTests.cs | 68 +----- src/Tests/CommonTests.fs | 53 ++++- src/Tests/PostgresTests.fs | 126 ----------- src/Tests/SqliteTests.fs | 59 ----- 10 files changed, 255 insertions(+), 734 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index a1cc7a2..4ee5c4f 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -214,11 +214,6 @@ module Query = let statementWhere statement where = $"%s{statement} WHERE %s{where}" - /// Create a SELECT clause to retrieve the document data from the given table - [] - let selectFromTable tableName = - $"SELECT data FROM %s{tableName}" - /// Queries to define tables and indexes module Definition = @@ -263,11 +258,6 @@ module Query = "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - /// Query to update a document (no WHERE clause) - [] - let update tableName = - $"UPDATE %s{tableName} SET data = @data" - /// Query to count documents in a table (no WHERE clause) [] let count tableName = @@ -277,3 +267,24 @@ module Query = [] let exists tableName where = $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it" + + /// Query to select documents from a table (no WHERE clause) + [] + let find tableName = + $"SELECT data FROM %s{tableName}" + + /// Query to update a document (no WHERE clause) + [] + let update tableName = + $"UPDATE %s{tableName} SET data = @data" + + /// Query to delete documents from a table (no WHERE clause) + [] + let delete tableName = + $"DELETE FROM %s{tableName}" + + /// Create a SELECT clause to retrieve the document data from the given table + [] + [] + let selectFromTable tableName = + find tableName diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index b297eb9..818d11b 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -396,7 +396,7 @@ type NpgsqlConnectionCSharpExtensions = /// Remove fields from a document by the document's ID [] static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) = - WithProps.RemoveFields.ById(tableName, docId, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn) /// Remove fields from documents via a comparison on JSON fields in the document [] @@ -412,12 +412,12 @@ type NpgsqlConnectionCSharpExtensions = /// Remove fields from documents via a JSON containment query (@>) [] static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) = - WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn) /// Remove fields from documents via a JSON Path match query (@?) [] static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) = - WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (Sql.existingConnection conn) /// Delete a document by its ID [] diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 69f86e0..b96d322 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -56,6 +56,7 @@ module Parameters = /// Create an ID parameter (name "@id", key will be treated as a string) [] let idParam (key: 'TKey) = + // TODO: bind key by numeric types "@id", Sql.string (string key) /// Create a parameter with a JSON value @@ -170,141 +171,37 @@ module Query = let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" + /// Create an UPDATE statement to patch documents + [] + let patch tableName = + $"UPDATE %s{tableName} SET data = data || @data" + + /// Create an UPDATE statement to remove fields from documents + [] + let removeFields tableName = + $"UPDATE %s{tableName} SET data = data - @name" + + /// Create a query by a document's ID + [] + let byId<'TKey> statement (docId: 'TKey) = + Query.statementWhere + statement + (whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ]) + /// Create a query on JSON fields - let fieldQuery statement howMatched fields = + [] + let byFields statement howMatched fields = Query.statementWhere statement (whereByFields howMatched fields) /// Create a JSON containment query - let containQuery statement = + [] + let byContains statement = Query.statementWhere statement (whereDataContains "@criteria") /// Create a JSON Path match query - let pathMatchQuery statement = + [] + let byPathMatch statement = Query.statementWhere statement (whereJsonPathMatches "@path") - - /// Queries for retrieving documents - module Find = - - /// Query to retrieve a document by its ID - [] - let byId tableName = - $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" - - /// Query to retrieve documents using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"{Query.selectFromTable tableName} WHERE {whereByFields howMatched fields}" - - /// Query to retrieve documents using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to retrieve documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to retrieve documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" - - /// Queries to patch (partially update) documents - module Patch = - - /// Create an UPDATE statement to patch documents - let private update tableName whereClause = - $"UPDATE %s{tableName} SET data = data || @data WHERE {whereClause}" - - /// Query to patch a document by its ID - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to patch documents match JSON field comparisons (->> =) - [] - let byFields tableName howMatched fields = - whereByFields howMatched fields |> update tableName - - /// Query to patch documents match a JSON field comparison (->> =) - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byContains tableName = - whereDataContains "@criteria" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byJsonPath tableName = - whereJsonPathMatches "@path" |> update tableName - - /// Queries to remove fields from documents - module RemoveFields = - - /// Create an UPDATE statement to remove parameters - let private update tableName whereClause = - $"UPDATE %s{tableName} SET data = data - @name WHERE {whereClause}" - - /// Query to remove fields from a document by the document's ID - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to remove fields from documents via a comparison on JSON fields within the document - [] - let byFields tableName howMatched fields = - whereByFields howMatched fields |> update tableName - - /// Query to remove fields from documents via a comparison on a JSON field within the document - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byContains tableName = - whereDataContains "@criteria" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byJsonPath tableName = - whereJsonPathMatches "@path" |> update tableName - - /// Queries to delete documents - module Delete = - - /// Query to delete a document by its ID - [] - let byId tableName = - $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - - /// Query to delete documents using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"DELETE FROM %s{tableName} WHERE {whereByFields howMatched fields}" - - /// Query to delete documents using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Query to delete documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to delete documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" /// Functions for dealing with results @@ -434,19 +331,19 @@ module WithProps = [] let byFields tableName howMatched fields sqlProps = Custom.scalar - (Query.fieldQuery (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount sqlProps + (Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount sqlProps /// Count matching documents using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = Custom.scalar - (Query.containQuery (Query.count tableName)) [ jsonParam "@criteria" criteria ] toCount sqlProps + (Query.byContains (Query.count tableName)) [ jsonParam "@criteria" criteria ] toCount sqlProps /// Count matching documents using a JSON Path match query (@?) [] let byJsonPath tableName jsonPath sqlProps = Custom.scalar - (Query.pathMatchQuery (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps + (Query.byPathMatch (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps /// Commands to determine if documents exist [] @@ -491,27 +388,31 @@ module WithProps = /// Retrieve all documents in the given table [] let all<'TDoc> tableName sqlProps = - Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps + Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> sqlProps /// Retrieve all documents in the given table let All<'TDoc>(tableName, sqlProps) = - Custom.List<'TDoc>(Query.selectFromTable tableName, [], fromData<'TDoc>, sqlProps) + Custom.List<'TDoc>(Query.find tableName, [], fromData<'TDoc>, sqlProps) /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps = - Custom.single (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> sqlProps + Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps /// Retrieve a document by its ID (returns null if not found) let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = - Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, sqlProps) + Custom.Single<'TDoc>( + Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps) /// Retrieve documents matching JSON field comparisons (->> =) [] let byFields<'TDoc> tableName howMatched fields sqlProps = Custom.list<'TDoc> - (Query.Find.byFields tableName howMatched fields) (addFieldParams fields []) fromData<'TDoc> sqlProps - + (Query.byFields (Query.find tableName) howMatched fields) + (addFieldParams fields []) + fromData<'TDoc> + sqlProps + /// Retrieve documents matching a JSON field comparison (->> =) [] [] @@ -521,7 +422,10 @@ module WithProps = /// Retrieve documents matching JSON field comparisons (->> =) let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) = Custom.List<'TDoc>( - Query.Find.byFields tableName howMatched fields, addFieldParams fields [], fromData<'TDoc>, sqlProps) + Query.byFields (Query.find tableName) howMatched fields, + addFieldParams fields [], + fromData<'TDoc>, + sqlProps) /// Retrieve documents matching a JSON field comparison (->> =) [] @@ -532,29 +436,35 @@ module WithProps = [] let byContains<'TDoc> tableName (criteria: obj) sqlProps = Custom.list<'TDoc> - (Query.Find.byContains tableName) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps + (Query.byContains (Query.find tableName)) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps /// Retrieve documents matching a JSON containment query (@>) let ByContains<'TDoc>(tableName, criteria: obj, sqlProps) = Custom.List<'TDoc>( - Query.Find.byContains tableName, [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps) + Query.byContains (Query.find tableName), + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) /// Retrieve documents matching a JSON Path match query (@?) [] let byJsonPath<'TDoc> tableName jsonPath sqlProps = Custom.list<'TDoc> - (Query.Find.byJsonPath tableName) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + (Query.byPathMatch (Query.find tableName)) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps /// Retrieve documents matching a JSON Path match query (@?) let ByJsonPath<'TDoc>(tableName, jsonPath, sqlProps) = Custom.List<'TDoc>( - Query.Find.byJsonPath tableName, [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) + Query.byPathMatch (Query.find tableName), + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) /// Retrieve the first document matching JSON field comparisons (->> =); returns None if not found [] let firstByFields<'TDoc> tableName howMatched fields sqlProps = Custom.single<'TDoc> - $"{Query.Find.byFields tableName howMatched fields} LIMIT 1" + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1" (addFieldParams fields []) fromData<'TDoc> sqlProps @@ -568,7 +478,7 @@ module WithProps = /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byFields tableName howMatched fields} LIMIT 1", + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", addFieldParams fields [], fromData<'TDoc>, sqlProps) @@ -582,12 +492,15 @@ module WithProps = [] let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = Custom.single<'TDoc> - $"{Query.Find.byContains tableName} LIMIT 1" [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps + $"{Query.byContains (Query.find tableName)} LIMIT 1" + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps /// Retrieve the first document matching a JSON containment query (@>); returns null if not found let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byContains tableName} LIMIT 1", + $"{Query.byContains (Query.find tableName)} LIMIT 1", [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps) @@ -596,12 +509,15 @@ module WithProps = [] let firstByJsonPath<'TDoc> tableName jsonPath sqlProps = Custom.single<'TDoc> - $"{Query.Find.byJsonPath tableName} LIMIT 1" [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + $"{Query.byPathMatch (Query.find tableName)} LIMIT 1" + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byJsonPath tableName} LIMIT 1", + $"{Query.byPathMatch (Query.find tableName)} LIMIT 1", [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) @@ -634,13 +550,14 @@ module WithProps = /// Patch a document by its ID [] let byId tableName (docId: 'TKey) (patch: 'TPatch) sqlProps = - Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] sqlProps + Custom.nonQuery + (Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] sqlProps /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] let byFields tableName howMatched fields (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byFields tableName howMatched fields) + (Query.byFields (Query.patch tableName) howMatched fields) (addFieldParams fields [ jsonParam "@data" patch ]) sqlProps @@ -654,32 +571,33 @@ module WithProps = [] let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byContains tableName) [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] sqlProps + (Query.byContains (Query.patch tableName)) + [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] + sqlProps /// Patch documents using a JSON Path match query in the WHERE clause (@?) [] let byJsonPath tableName jsonPath (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byJsonPath tableName) [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] sqlProps + (Query.byPathMatch (Query.patch tableName)) + [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] + sqlProps /// Commands to remove fields from documents [] module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames sqlProps = - Custom.nonQuery (Query.RemoveFields.byId tableName) [ idParam docId; fieldNameParams fieldNames ] sqlProps - - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames, sqlProps) = - byId tableName docId (List.ofSeq fieldNames) sqlProps + Custom.nonQuery + (Query.byId (Query.removeFields tableName) docId) [ idParam docId; fieldNameParams fieldNames ] sqlProps /// Remove fields from documents via a comparison on JSON fields in the document [] let byFields tableName howMatched fields fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byFields tableName howMatched fields) + (Query.byFields (Query.removeFields tableName) howMatched fields) (addFieldParams fields [ fieldNameParams fieldNames ]) sqlProps @@ -690,29 +608,21 @@ module WithProps = byFields tableName Any [ field ] fieldNames sqlProps /// Remove fields from documents via a JSON containment query (@>) - [] + [] let byContains tableName (criteria: 'TContains) fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byContains tableName) + (Query.byContains (Query.removeFields tableName)) [ jsonParam "@criteria" criteria; fieldNameParams fieldNames ] sqlProps - - /// Remove fields from documents via a JSON containment query (@>) - let ByContains(tableName, criteria: 'TContains, fieldNames, sqlProps) = - byContains tableName criteria (List.ofSeq fieldNames) sqlProps - + /// Remove fields from documents via a JSON Path match query (@?) [] let byJsonPath tableName jsonPath fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byJsonPath tableName) + (Query.byPathMatch (Query.removeFields tableName)) [ "@path", Sql.string jsonPath; fieldNameParams fieldNames ] sqlProps - /// Remove fields from documents via a JSON Path match query (@?) - let ByJsonPath(tableName, jsonPath, fieldNames, sqlProps) = - byJsonPath tableName jsonPath (List.ofSeq fieldNames) sqlProps - /// Commands to delete documents [] module Delete = @@ -720,12 +630,13 @@ module WithProps = /// Delete a document by its ID [] let byId tableName (docId: 'TKey) sqlProps = - Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps + Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] sqlProps /// Delete documents by matching a JSON field comparison query (->> =) [] let byFields tableName howMatched fields sqlProps = - Custom.nonQuery (Query.Delete.byFields tableName howMatched fields) (addFieldParams fields []) sqlProps + Custom.nonQuery + (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) sqlProps /// Delete documents by matching a JSON field comparison query (->> =) [] @@ -736,14 +647,14 @@ module WithProps = /// Delete documents by matching a JSON contains query (@>) [] let byContains tableName (criteria: 'TCriteria) sqlProps = - Custom.nonQuery (Query.Delete.byContains tableName) [ jsonParam "@criteria" criteria ] sqlProps + Custom.nonQuery (Query.byContains (Query.delete tableName)) [ jsonParam "@criteria" criteria ] sqlProps /// Delete documents by matching a JSON Path match query (@?) [] let byJsonPath tableName path sqlProps = - Custom.nonQuery (Query.Delete.byJsonPath tableName) [ "@path", Sql.string path ] sqlProps - - + Custom.nonQuery (Query.byPathMatch (Query.delete tableName)) [ "@path", Sql.string path ] sqlProps + + /// Commands to execute custom SQL queries [] module Custom = @@ -1032,14 +943,10 @@ module Patch = module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames = WithProps.RemoveFields.byId tableName docId fieldNames (fromDataSource ()) - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames) = - WithProps.RemoveFields.ById(tableName, docId, fieldNames, fromDataSource ()) - /// Remove fields from documents via a comparison on JSON fields in the document [] let byFields tableName howMatched fields fieldNames = @@ -1052,24 +959,16 @@ module RemoveFields = byFields tableName Any [ field ] fieldNames /// Remove fields from documents via a JSON containment query (@>) - [] + [] let byContains tableName (criteria: 'TContains) fieldNames = WithProps.RemoveFields.byContains tableName criteria fieldNames (fromDataSource ()) - /// Remove fields from documents via a JSON containment query (@>) - let ByContains(tableName, criteria: 'TContains, fieldNames) = - WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, fromDataSource ()) - /// Remove fields from documents via a JSON Path match query (@?) - [] + [] let byJsonPath tableName jsonPath fieldNames = WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (fromDataSource ()) - /// Remove fields from documents via a JSON Path match query (@?) - let ByJsonPath(tableName, jsonPath, fieldNames) = - WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, fromDataSource ()) - /// Commands to delete documents [] module Delete = diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index a0d861a..6929be9 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -56,6 +56,29 @@ module Query = let whereById paramName = whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] + /// Create an UPDATE statement to patch documents + [] + let patch tableName = + $"UPDATE %s{tableName} SET data = json_patch(data, json(@data))" + + /// Create an UPDATE statement to remove fields from documents + [] + let removeFields tableName (parameters: SqliteParameter seq) = + let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", " + $"UPDATE %s{tableName} SET data = json_remove(data, {paramNames})" + + /// Create a query by a document's ID + [] + let byId<'TKey> statement (docId: 'TKey) = + Query.statementWhere + statement + (whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ]) + + /// Create a query on JSON fields + [] + let byFields statement howMatched fields = + Query.statementWhere statement (whereByFields howMatched fields) + /// Data definition module Definition = @@ -63,92 +86,7 @@ module Query = [] let ensureTable name = Query.Definition.ensureTableFor name "TEXT" - - /// Queries for retrieving documents - module Find = - /// Query to retrieve a document by its ID - [] - let byId tableName = - $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" - - /// Query to retrieve documents using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"{Query.selectFromTable tableName} WHERE {whereByFields howMatched fields}" - - /// Query to retrieve documents using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Document patching (partial update) queries - module Patch = - - /// Create an UPDATE statement to patch documents - let internal update tableName whereClause = - $"UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE %s{whereClause}" - - /// Query to patch (partially update) a document by its ID - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to patch (partially update) a document via a comparison on JSON fields - [] - let byFields tableName howMatched fields = - whereByFields howMatched fields |> update tableName - - /// Query to patch (partially update) a document via a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - - /// Queries to remove fields from documents - module RemoveFields = - - /// Create an UPDATE statement to remove parameters - let internal update tableName (parameters: SqliteParameter seq) whereClause = - let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", " - $"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE %s{whereClause}" - - /// Query to remove fields from a document by the document's ID - [] - let byId tableName parameters = - whereById "@id" |> update tableName parameters - - /// Query to remove fields from documents via a comparison on JSON fields within the document - [] - let byFields tableName howMatched fields parameters = - whereByFields howMatched fields |> update tableName parameters - - /// Query to remove fields from documents via a comparison on a JSON field within the document - [] - [] - let byField tableName field parameters = - byFields tableName Any [ field ] parameters - - /// Queries to delete documents - module Delete = - - /// Query to delete a document by its ID - [] - let byId tableName = - $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - - /// Query to delete documents using a comparison on JSON fields - [] - let byFields tableName howMatched fields = - $"DELETE FROM %s{tableName} WHERE {whereByFields howMatched fields}" - - /// Query to delete documents using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - /// Parameter handling helpers [] @@ -349,10 +287,7 @@ module WithConn = [] let byFields tableName howMatched fields conn = Custom.scalar - (Query.statementWhere (Query.count tableName) (Query.whereByFields howMatched fields)) - (addFieldParams fields []) - toCount - conn + (Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount conn /// Commands to determine if documents exist [] @@ -379,27 +314,30 @@ module WithConn = /// Retrieve all documents in the given table [] let all<'TDoc> tableName conn = - Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn + Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> conn /// Retrieve all documents in the given table let All<'TDoc>(tableName, conn) = - Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn) + Custom.List(Query.find tableName, [], fromData<'TDoc>, conn) /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn = - Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn + Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn /// Retrieve a document by its ID (returns null if not found) let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = - Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn) + Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn) /// Retrieve documents via a comparison on JSON fields [] let byFields<'TDoc> tableName howMatched fields conn = Custom.list<'TDoc> - (Query.Find.byFields tableName howMatched fields) (addFieldParams fields []) fromData<'TDoc> conn - + (Query.byFields (Query.find tableName) howMatched fields) + (addFieldParams fields []) + fromData<'TDoc> + conn + /// Retrieve documents via a comparison on a JSON field [] [] @@ -409,8 +347,11 @@ module WithConn = /// Retrieve documents via a comparison on JSON fields let ByFields<'TDoc>(tableName, howMatched, fields, conn) = Custom.List<'TDoc>( - Query.Find.byFields tableName howMatched fields, addFieldParams fields [], fromData<'TDoc>, conn) - + Query.byFields (Query.find tableName) howMatched fields, + addFieldParams fields [], + fromData<'TDoc>, + conn) + /// Retrieve documents via a comparison on a JSON field [] let ByField<'TDoc>(tableName, field, conn) = @@ -420,7 +361,7 @@ module WithConn = [] let firstByFields<'TDoc> tableName howMatched fields conn = Custom.single - $"{Query.Find.byFields tableName howMatched fields} LIMIT 1" + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1" (addFieldParams fields []) fromData<'TDoc> conn @@ -434,7 +375,7 @@ module WithConn = /// Retrieve documents via a comparison on JSON fields, returning only the first result let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = Custom.Single( - $"{Query.Find.byFields tableName howMatched fields} LIMIT 1", + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", addFieldParams fields [], fromData<'TDoc>, conn) @@ -472,13 +413,14 @@ module WithConn = /// Patch a document by its ID [] let byId tableName (docId: 'TKey) (patch: 'TPatch) conn = - Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] conn + Custom.nonQuery + (Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] conn /// Patch documents using a comparison on JSON fields [] let byFields tableName howMatched fields (patch: 'TPatch) conn = Custom.nonQuery - (Query.Patch.byFields tableName howMatched fields) + (Query.byFields (Query.patch tableName) howMatched fields) (addFieldParams fields [ jsonParam "@data" patch ]) conn @@ -497,7 +439,7 @@ module WithConn = let byId tableName (docId: 'TKey) fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames Custom.nonQuery - (Query.RemoveFields.byId tableName nameParams) + (Query.byId (Query.removeFields tableName nameParams) docId) (idParam docId |> Seq.singleton |> Seq.append nameParams) conn @@ -506,10 +448,10 @@ module WithConn = let byFields tableName howMatched fields fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames Custom.nonQuery - (Query.RemoveFields.byFields tableName howMatched fields nameParams) + (Query.byFields (Query.removeFields tableName nameParams) howMatched fields) (addFieldParams fields nameParams) conn - + /// Remove fields from documents via a comparison on a JSON field in the document [] [] @@ -523,13 +465,13 @@ module WithConn = /// Delete a document by its ID [] let byId tableName (docId: 'TKey) conn = - Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn + Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] conn /// Delete documents by matching a comparison on JSON fields [] let byFields tableName howMatched fields conn = - Custom.nonQuery (Query.Delete.byFields tableName howMatched fields) (addFieldParams fields []) conn - + Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn + /// Delete documents by matching a comparison on a JSON field [] [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index d1ed8cd..fa60bb8 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -238,10 +238,9 @@ public static class CommonCSharpTests ]), TestList("Query", [ - TestCase("SelectFromTable succeeds", () => + TestCase("StatementWhere succeeds", () => { - Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", - "SELECT statement not correct"); + Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); }), TestList("Definition", [ @@ -266,15 +265,32 @@ public static class CommonCSharpTests "CREATE INDEX for key statement without schema not constructed correctly"); }) ]), - TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("test.table", "gibberish", - new[] { "taco", "guac DESC", "salsa ASC" }, Dialect.SQLite), - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", - "CREATE INDEX for multiple field statement incorrect"); - }) + TestList("EnsureIndexOn", + [ + TestCase("succeeds for multiple fields and directions", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("test.table", "gibberish", + new[] { "taco", "guac DESC", "salsa ASC" }, Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + "CREATE INDEX for multiple field statement incorrect"); + }), + TestCase("succeeds for nested PostgreSQL field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", + "CREATE INDEX for nested PostgreSQL field incorrect"); + }), + TestCase("succeeds for nested SQLite field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", + "CREATE INDEX for nested SQLite field incorrect"); + }) + ]) ]), TestCase("Insert succeeds", () => { @@ -285,6 +301,27 @@ public static class CommonCSharpTests Expect.equal(Query.Save("tbl"), "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); + }), + TestCase("Count succeeds", () => + { + Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); + }), + TestCase("Exists succeeds", () => + { + Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", + "Exists query not correct"); + }), + TestCase("Find succeeds", () => + { + Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); + }), + TestCase("Update succeeds", () => + { + Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); + }), + TestCase("Delete succeeds", () => + { + Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); }) ]) ]); diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index cc115fd..71ff804 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -279,153 +279,7 @@ public static class PostgresCSharpTests { Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", "WHERE clause not correct"); - }), - TestList("Find", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", - "SELECT by ID query not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.Find.ByFields("x", FieldMatch.Any, [Field.GE("Golf", 0), Field.LE("Flog", 1)]), - $"SELECT data FROM x WHERE data->>'Golf' >= @field0 OR data->>'Flog' <= @field1", - "SELECT by JSON comparison query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)), - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field0", - "SELECT by JSON comparison query not correct"); - }), -#pragma warning restore CS0618 - TestCase("byContains succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria", - "SELECT by JSON containment query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByJsonPath(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "SELECT by JSON Path match query not correct"); - }) - ]), - TestList("Patch", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.Patch.ByFields("x", FieldMatch.All, - [Field.LT("Snail", 0), Field.BT("Slug", 8, 14)]), - $"UPDATE x SET data = data || @data WHERE data->>'Snail' < @field0 AND (data->>'Slug')::numeric BETWEEN @field1min AND @field1max", - "UPDATE partial by ID statement not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field0", - "UPDATE partial by ID statement not correct"); - }), -#pragma warning restore CS0618 - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByContains(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", - "UPDATE partial by JSON containment statement not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByJsonPath(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", - "UPDATE partial by JSON Path statement not correct"); - }) - ]), - TestList("RemoveFields", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id", - "Remove field by ID query not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.RemoveFields.ByFields("x", FieldMatch.Any, - [Field.LT("Fly", 0), Field.LT("Ant", 2)]), - $"UPDATE x SET data = data - @name WHERE data->>'Fly' < @field0 OR data->>'Ant' < @field1", - "Remove field by field query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByField(PostgresDb.TableName, Field.LT("Fly", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field0", - "Remove field by field query not correct"); - }), -#pragma warning restore CS0618 - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByContains(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @> @criteria", - "Remove field by contains query not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByJsonPath(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath", - "Remove field by JSON path query not correct"); - }) - ]), - TestList("Delete", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", - "DELETE by ID query not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal( - Postgres.Query.Delete.ByFields("tbl", FieldMatch.All, [Field.NEX("gone"), Field.EX("here")]), - $"DELETE FROM tbl WHERE data->>'gone' IS NULL AND data->>'here' IS NOT NULL", - "DELETE by JSON comparison query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByField(PostgresDb.TableName, Field.NEX("gone")), - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL", - "DELETE by JSON comparison query not correct"); - }), -#pragma warning restore CS0618 - TestCase("byContains succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria", - "DELETE by JSON containment query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByJsonPath(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "DELETE by JSON Path match query not correct"); - }) - ]) + }) ]) ]); diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index be07d87..3aa1f86 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -91,73 +91,7 @@ public static class SqliteCSharpTests { Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); - }), - TestList("Find", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", - "SELECT by ID query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Find.ByField("tbl", Field.GE("Golf", 0)), - "SELECT data FROM tbl WHERE data->>'Golf' >= @field0", - "SELECT by JSON comparison query not correct"); - }) -#pragma warning restore CS0618 - ]), - TestList("Patch", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Patch.ById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Patch.ByField("tbl", Field.NE("Part", 0)), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field0", - "UPDATE partial by JSON comparison query not correct"); - }) -#pragma warning restore CS0618 - ]), - TestList("RemoveFields", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }), - "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id", - "Remove field by ID query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0), - new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }), - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field0", - "Remove field by field query not correct"); - }) -#pragma warning restore CS0618 - ]), - TestList("Delete", - [ - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", - "DELETE by ID query not correct"); - }), -#pragma warning disable CS0618 - TestCase("ByField succeeds", () => - { - 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"); - }) -#pragma warning restore CS0618 - ]) + }) ]), TestList("Parameters", [ diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 4e2eab0..edc5cb1 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -170,8 +170,8 @@ let all = } ] testList "Query" [ - test "selectFromTable succeeds" { - Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" + test "statementWhere succeeds" { + Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly" } testList "Definition" [ test "ensureTableFor succeeds" { @@ -194,15 +194,29 @@ let all = "CREATE INDEX for key statement without schema not constructed correctly" } ] - test "ensureIndexOn succeeds for multiple fields and directions" { - Expect.equal - (Query.Definition.ensureIndexOn - "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) - ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] - |> String.concat "") - "CREATE INDEX for multiple field statement incorrect" - } + testList "ensureIndexOn" [ + test "succeeds for multiple fields and directions" { + Expect.equal + (Query.Definition.ensureIndexOn + "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) + ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] + |> String.concat "") + "CREATE INDEX for multiple field statement incorrect" + } + test "succeeds for nested PostgreSQL field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))" + "CREATE INDEX for nested PostgreSQL field incorrect" + } + test "succeeds for nested SQLite field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))" + "CREATE INDEX for nested SQLite field incorrect" + } + ] ] test "insert succeeds" { Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" @@ -214,7 +228,22 @@ let all = "INSERT ON CONFLICT UPDATE statement not correct" } test "count succeeds" { - Expect.equal (Query.count "a_table") "SELECT COUNT(*) AS it FROM a_table" "Count query not correct" + Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct" + } + test "exists succeeds" { + Expect.equal + (Query.exists tbl "turkey") + $"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it" + "Exists query not correct" + } + test "find succeeds" { + Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct" + } + test "update succeeds" { + Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct" + } + test "delete succeeds" { + Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct" } ] ] diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index d217a2b..e39bbb9 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -252,132 +252,6 @@ let unitTests = test "whereJsonPathMatches succeeds" { Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" } - testList "Find" [ - test "byId succeeds" { - Expect.equal - (Query.Find.byId PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" - "SELECT by ID query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.Find.byFields "tbl" Any [ Field.GE "Golf" 0; Field.LE "Flog" 1 ]) - $"SELECT data FROM tbl WHERE data->>'Golf' >= @field0 OR data->>'Flog' <= @field1" - "SELECT by JSON comparison query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0)) - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field0" - "SELECT by JSON comparison query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Find.byContains PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria" - "SELECT by JSON containment query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Find.byJsonPath PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "SELECT by JSON Path match query not correct" - } - ] - testList "Patch" [ - test "byId succeeds" { - Expect.equal - (Query.Patch.byId PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.Patch.byFields "x" All [ Field.LT "Snail" 0; Field.BT "Slug" 8 12 ]) - $"UPDATE x SET data = data || @data WHERE data->>'Snail' < @field0 AND (data->>'Slug')::numeric BETWEEN @field1min AND @field1max" - "UPDATE partial by ID statement not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field0" - "UPDATE partial by ID statement not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Patch.byContains PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" - "UPDATE partial by JSON containment statement not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Patch.byJsonPath PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" - "UPDATE partial by JSON Path statement not correct" - } - ] - testList "RemoveFields" [ - test "byId succeeds" { - Expect.equal - (Query.RemoveFields.byId PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id" - "Remove field by ID query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.RemoveFields.byFields "tbl" Any [ Field.LT "Fly" 0; Field.LT "Ant" 2 ]) - "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field0 OR data->>'Ant' < @field1" - "Remove field by field query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.RemoveFields.byField PostgresDb.TableName (Field.LT "Fly" 0)) - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field0" - "Remove field by field query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.RemoveFields.byContains PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @> @criteria" - "Remove field by contains query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.RemoveFields.byJsonPath PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath" - "Remove field by JSON path query not correct" - } - ] - testList "Delete" [ - test "byId succeeds" { - Expect.equal - (Query.Delete.byId PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" - "DELETE by ID query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.Delete.byFields PostgresDb.TableName All [ Field.NEX "gone"; Field.EX "here" ]) - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL AND data->>'here' IS NOT NULL" - "DELETE by JSON comparison query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Delete.byField PostgresDb.TableName (Field.NEX "gone")) - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL" - "DELETE by JSON comparison query not correct" - } - test "byContains succeeds" { - Expect.equal (Query.Delete.byContains PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" - "DELETE by JSON containment query not correct" - } - test "byJsonPath succeeds" { - Expect.equal (Query.Delete.byJsonPath PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "DELETE by JSON Path match query not correct" - } - ] ] ] diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index a916473..a19d7d3 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -81,65 +81,6 @@ let unitTests = "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" "CREATE TABLE statement not correct" } - testList "Find" [ - test "byId succeeds" { - Expect.equal - (Query.Find.byId "tbl") - "SELECT data FROM tbl WHERE data->>'Id' = @id" - "SELECT by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Find.byField "tbl" (Field.GE "Golf" 0)) - "SELECT data FROM tbl WHERE data->>'Golf' >= @field0" - "SELECT by JSON comparison query not correct" - } - ] - testList "Patch" [ - test "byId succeeds" { - Expect.equal - (Query.Patch.byId "tbl") - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Patch.byField "tbl" (Field.NE "Part" 0)) - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field0" - "UPDATE partial by JSON comparison query not correct" - } - ] - testList "RemoveFields" [ - test "byId succeeds" { - Expect.equal - (Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ]) - "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id" - "Remove field by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.RemoveFields.byField - "tbl" - (Field.GT "Fly" 0) - [ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ]) - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' > @field0" - "Remove field by field query not correct" - } - ] - testList "Delete" [ - test "byId succeeds" { - Expect.equal - (Query.Delete.byId "tbl") - "DELETE FROM tbl WHERE data->>'Id' = @id" - "DELETE by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Delete.byField "tbl" (Field.NEX "gone")) - "DELETE FROM tbl WHERE data->>'gone' IS NULL" - "DELETE by JSON comparison query not correct" - } - ] ] testList "Parameters" [ test "idParam succeeds" { -- 2.45.1 From e0fb793ec98742e74a0c564de13251f5b42d6cdd Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 10 Aug 2024 15:07:59 -0400 Subject: [PATCH 13/29] WIP on Postgres type derivation --- src/Postgres/Library.fs | 23 +++++++++++------- src/Sqlite/Library.fs | 6 ----- src/Tests.CSharp/PostgresCSharpTests.cs | 25 ------------------- src/Tests.CSharp/SqliteCSharpTests.cs | 20 ---------------- src/Tests/PostgresTests.fs | 32 +++---------------------- src/Tests/SqliteTests.fs | 20 ---------------- 6 files changed, 18 insertions(+), 108 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index b96d322..ce48e74 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -56,8 +56,21 @@ module Parameters = /// Create an ID parameter (name "@id", key will be treated as a string) [] let idParam (key: 'TKey) = - // TODO: bind key by numeric types - "@id", Sql.string (string key) + match box key with + | :? int8 as it -> Sql.int8 it + | :? uint8 as it -> Sql.int8 (int8 it) + | :? int16 as it -> Sql.int16 it + | :? uint16 as it -> Sql.int16 (int16 it) + | :? int as it -> Sql.int it + | :? uint32 as it -> Sql.int (int it) + | :? int64 as it -> Sql.int64 it + | :? uint64 as it -> Sql.int64 (int64 it) + | :? decimal as it -> Sql.decimal it + | :? single as it -> Sql.double (double it) + | :? double as it -> Sql.double it + | :? string as it -> Sql.string it + | _ -> Sql.string (string key) + |> function it -> "@id", it /// Create a parameter with a JSON value [] @@ -135,12 +148,6 @@ module Query = | _ -> $"{it.Path PostgreSQL} {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 a field in a JSON document - [] - [] - let whereByField field paramName = - whereByFields Any [ { field with ParameterName = Some paramName } ] - /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 6929be9..7acc7de 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -45,12 +45,6 @@ module Query = | _ -> $"{it.Path SQLite} {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 a field in a JSON document - [] - [] - let whereByField field paramName = - whereByFields Any [ { field with ParameterName = Some paramName } ] - /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 71ff804..6e6d4c3 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -217,31 +217,6 @@ public static class PostgresCSharpTests "WHERE clause not correct"); }) ]), -#pragma warning disable CS0618 - TestList("WhereByField", - [ - TestCase("succeeds when a logical operator is passed", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds when an existence operator is passed", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL", - "WHERE clause not correct"); - }), - TestCase("succeeds when a between operator is passed with numeric values", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), - "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); - }), - TestCase("succeeds when a between operator is passed with non-numeric values", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"), - "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); - }) - ]), -#pragma warning restore CS0618 TestCase("WhereById succeeds", () => { Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 3aa1f86..60bfd09 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -63,26 +63,6 @@ public static class SqliteCSharpTests "WHERE clause not correct"); }) ]), -#pragma warning disable CS0618 - TestList("WhereByField", - [ - TestCase("succeeds when a logical operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds when an existence operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL", - "WHERE clause not correct"); - }), - TestCase("succeeds when the between operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), - "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); - }) - ]), -#pragma warning restore CS0618 TestCase("WhereById succeeds", () => { Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index e39bbb9..fdd9068 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -196,32 +196,6 @@ let unitTests = "WHERE clause not correct" } ] - testList "whereByField" [ - test "succeeds when a logical operator is passed" { - Expect.equal - (Query.whereByField (Field.GT "theField" 0) "@test") - "data->>'theField' > @test" - "WHERE clause not correct" - } - test "succeeds when an existence operator is passed" { - Expect.equal - (Query.whereByField (Field.NEX "thatField") "") - "data->>'thatField' IS NULL" - "WHERE clause not correct" - } - test "succeeds when a between operator is passed with numeric values" { - Expect.equal - (Query.whereByField (Field.BT "aField" 50 99) "@range") - "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax" - "WHERE clause not correct" - } - test "succeeds when a between operator is passed with non-numeric values" { - Expect.equal - (Query.whereByField (Field.BT "field0" "a" "b") "@alpha") - "data->>'field0' BETWEEN @alphamin AND @alphamax" - "WHERE clause not correct" - } - ] test "whereById succeeds" { Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" } @@ -466,7 +440,7 @@ let integrationTests = let! theCount = Count.all PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - ptestList "byFields" [ + testList "byFields" [ testTask "succeeds when items are found" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -531,7 +505,7 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "byFields" [ + ftestList "byFields" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -647,7 +621,7 @@ let integrationTests = PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] Expect.equal (List.length docs) 1 "There should have been one document returned" } - ptestTask "succeeds when documents are not found" { + ftestTask "succeeds when documents are not found" { use db = PostgresDb.BuildDb() do! loadDocs () diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index a19d7d3..911d5aa 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -52,26 +52,6 @@ let unitTests = "WHERE clause not correct" } ] - testList "whereByField" [ - test "succeeds when a logical operator is passed" { - Expect.equal - (Query.whereByField (Field.GT "theField" 0) "@test") - "data->>'theField' > @test" - "WHERE clause not correct" - } - test "succeeds when an existence operator is passed" { - Expect.equal - (Query.whereByField (Field.NEX "thatField") "") - "data->>'thatField' IS NULL" - "WHERE clause not correct" - } - test "succeeds when the between operator is passed" { - Expect.equal - (Query.whereByField (Field.BT "aField" 50 99) "@range") - "data->>'aField' BETWEEN @rangemin AND @rangemax" - "WHERE clause not correct" - } - ] test "whereById succeeds" { Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" } -- 2.45.1 From d9d37f110d5d57319f07a04c96260c062e83f7a1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 10 Aug 2024 19:51:13 -0400 Subject: [PATCH 14/29] Add tests for PG parameter types - Alter whereById to expect a parameter --- src/Postgres/Library.fs | 74 ++++----- src/Tests.CSharp/PostgresCSharpTests.cs | 155 ++++++++++++++----- src/Tests/PostgresTests.fs | 195 ++++++++++++++---------- 3 files changed, 270 insertions(+), 154 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index ce48e74..365be45 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -45,6 +45,23 @@ module private Helpers = let! _ = it () } + + /// Create a numerically-typed parameter, or use the given parameter derivation function if non-(numeric or string) + let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) = + match box value with + | :? int8 as it -> Sql.int8 it + | :? uint8 as it -> Sql.int8 (int8 it) + | :? int16 as it -> Sql.int16 it + | :? uint16 as it -> Sql.int16 (int16 it) + | :? int as it -> Sql.int it + | :? uint32 as it -> Sql.int (int it) + | :? int64 as it -> Sql.int64 it + | :? uint64 as it -> Sql.int64 (int64 it) + | :? decimal as it -> Sql.decimal it + | :? single as it -> Sql.double (double it) + | :? double as it -> Sql.double it + | :? string as it -> Sql.string it + | _ -> catchAllFunc value open BitBadger.Documents @@ -56,21 +73,7 @@ module Parameters = /// Create an ID parameter (name "@id", key will be treated as a string) [] let idParam (key: 'TKey) = - match box key with - | :? int8 as it -> Sql.int8 it - | :? uint8 as it -> Sql.int8 (int8 it) - | :? int16 as it -> Sql.int16 it - | :? uint16 as it -> Sql.int16 (int16 it) - | :? int as it -> Sql.int it - | :? uint32 as it -> Sql.int (int it) - | :? int64 as it -> Sql.int64 it - | :? uint64 as it -> Sql.int64 (int64 it) - | :? decimal as it -> Sql.decimal it - | :? single as it -> Sql.double (double it) - | :? double as it -> Sql.double it - | :? string as it -> Sql.string it - | _ -> Sql.string (string key) - |> function it -> "@id", it + "@id", parameterFor key (fun it -> Sql.string (string it)) /// Create a parameter with a JSON value [] @@ -89,11 +92,13 @@ module Parameters = | BT -> let p = name.Derive it.ParameterName let values = it.Value :?> obj list - yield ($"{p}min", Sql.parameter (NpgsqlParameter($"{p}min", List.head values))) - yield ($"{p}max", Sql.parameter (NpgsqlParameter($"{p}max", List.last values))) + yield ($"{p}min", + parameterFor (List.head values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}min", v)))) + yield ($"{p}max", + parameterFor (List.last values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}max", v)))) | _ -> let p = name.Derive it.ParameterName - yield (p, Sql.parameter (NpgsqlParameter(p, it.Value))) }) + yield (p, parameterFor it.Value (fun v -> Sql.parameter (NpgsqlParameter(p, v)))) }) |> Seq.collect id |> Seq.append parameters |> Seq.toList @@ -131,27 +136,30 @@ module Query = [] let whereByFields howMatched fields = let name = ParameterName() + let isNumeric (it: obj) = + match it with + | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 + | :? decimal | :? single | :? double -> true + | _ -> false fields |> Seq.map (fun it -> match it.Op with | EX | NEX -> $"{it.Path PostgreSQL} {it.Op}" - | BT -> - let p = name.Derive it.ParameterName - let path, value = + | _ -> + let p = name.Derive it.ParameterName + let param, value = match it.Op with | BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0] | _ -> p, it.Value - match value with - | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"({it.Path PostgreSQL})::numeric {it.Op} {path}" - | _ -> $"{it.Path PostgreSQL} {it.Op} {path}" - | _ -> $"{it.Path PostgreSQL} {it.Op} {name.Derive it.ParameterName}") + if isNumeric value then + $"({it.Path PostgreSQL})::numeric {it.Op} {param}" + else $"{it.Path PostgreSQL} {it.Op} {param}") |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") /// Create a WHERE clause fragment to implement an ID-based query [] - let whereById paramName = - whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] + let whereById<'TKey> (docId: 'TKey) = + whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ] /// Table and index definition queries module Definition = @@ -191,9 +199,7 @@ module Query = /// Create a query by a document's ID [] let byId<'TKey> statement (docId: 'TKey) = - Query.statementWhere - statement - (whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ]) + Query.statementWhere statement (whereById docId) /// Create a query on JSON fields [] @@ -359,7 +365,7 @@ module WithProps = /// Determine if a document exists for the given ID [] let byId tableName (docId: 'TKey) sqlProps = - Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists sqlProps + Custom.scalar (Query.exists tableName (Query.whereById docId)) [ idParam docId ] toExists sqlProps /// Determine if a document exists using JSON field comparisons (->> =) [] @@ -537,9 +543,7 @@ module WithProps = [] let byId tableName (docId: 'TKey) (document: 'TDoc) sqlProps = Custom.nonQuery - (Query.statementWhere (Query.update tableName) (Query.whereById "@id")) - [ idParam docId; jsonParam "@data" document ] - sqlProps + (Query.byId (Query.update tableName) docId) [ idParam docId; jsonParam "@data" document ] sqlProps /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 6e6d4c3..ff5d08a 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -20,12 +20,93 @@ public static class PostgresCSharpTests [ TestList("Parameters", [ - TestCase("Id succeeds", () => - { - var it = Parameters.Id(88); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@string("88"), "ID parameter value incorrect"); - }), + TestList("Id", + [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + TestCase("succeeds for byte ID", () => + { + var it = Parameters.Id((sbyte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned byte ID", () => + { + var it = Parameters.Id((byte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Unsigned byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for short ID", () => + { + var it = Parameters.Id((short)44); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(44), "Short ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned short ID", () => + { + var it = Parameters.Id((ushort)64); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(64), "Unsigned short ID parameter not constructed correctly"); + }), + TestCase("succeeds for integer ID", () => + { + var it = Parameters.Id(88); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@int(88), "ID parameter value incorrect"); + }), + TestCase("succeeds for unsigned integer ID", () => + { + var it = Parameters.Id((uint)889); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@int(889), "Unsigned int ID parameter not constructed correctly"); + }), + TestCase("succeeds for long ID", () => + { + var it = Parameters.Id((long)123); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(123), "Long ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned long ID", () => + { + var it = Parameters.Id((ulong)6464); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(6464), + "Unsigned long ID parameter not constructed correctly"); + }), + TestCase("succeeds for decimal ID", () => + { + var it = Parameters.Id((decimal)4.56); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@decimal((decimal)4.56), + "Decimal ID parameter not constructed correctly"); + }), + TestCase("succeeds for single ID", () => + { + var it = Parameters.Id((float)5.67); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double((float)5.67), + "Single ID parameter not constructed correctly"); + }), + TestCase("succeeds for double ID", () => + { + var it = Parameters.Id(6.78); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double(6.78), + "Double ID parameter not constructed correctly"); + }), + TestCase("succeeds for string ID", () => + { + var it = Parameters.Id("99"); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("99"), "ID parameter value incorrect"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + var it = Parameters.Id(new Uri("https://example.com")); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("https://example.com/"), + "Non-numeric, non-string parameter value incorrect"); + }) + ]), TestCase("Json succeeds", () => { var it = Parameters.Json("@test", new { Something = "good" }); @@ -40,10 +121,7 @@ public static class PostgresCSharpTests Expect.hasLength(paramList, 1, "There should have been a parameter added"); var (name, value) = paramList[0]; Expect.equal(name, "@field0", "Field parameter name not correct"); - if (!value.IsParameter) - { - Expect.isTrue(false, "The parameter was not a Parameter type"); - } + Expect.equal(value, Sql.@string("242"), "Field parameter value not correct"); }), TestCase("succeeds when multiple independent parameters are added", () => { @@ -52,22 +130,13 @@ public static class PostgresCSharpTests Expect.hasLength(paramList, 3, "There should have been 2 parameters added"); var (name, value) = paramList[0]; Expect.equal(name, "@id", "First field parameter name not correct"); - if (!value.IsString) - { - Expect.isTrue(false, "First parameter was not a String type"); - } + Expect.equal(value, Sql.@int(14), "First field parameter value not correct"); (name, value) = paramList[1]; Expect.equal(name, "@field0", "Second field parameter name not correct"); - if (!value.IsParameter) - { - Expect.isTrue(false, "Second parameter was not a Parameter type"); - } + Expect.equal(value, Sql.@string("you"), "Second field parameter value not correct"); (name, value) = paramList[2]; Expect.equal(name, "@field1", "Third parameter name not correct"); - if (!value.IsParameter) - { - Expect.isTrue(false, "Third parameter was not a Parameter type"); - } + Expect.equal(value, Sql.@string("them"), "Third parameter value not correct"); }), TestCase("succeeds when a parameter is not added", () => { @@ -81,16 +150,10 @@ public static class PostgresCSharpTests Expect.hasLength(paramList, 2, "There should have been 2 parameters added"); var (name, value) = paramList[0]; Expect.equal(name, "@testmin", "Minimum field name not correct"); - if (!value.IsParameter) - { - Expect.isTrue(false, "Minimum parameter was not a Parameter type"); - } + Expect.equal(value, Sql.@string("eh"), "Minimum field value not correct"); (name, value) = paramList[1]; Expect.equal(name, "@testmax", "Maximum field name not correct"); - if (!value.IsParameter) - { - Expect.isTrue(false, "Maximum parameter was not a Parameter type"); - } + Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); }) ]), #pragma warning disable CS0618 @@ -101,7 +164,7 @@ public static class PostgresCSharpTests 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"); + Expect.isTrue(it[0].Item2.IsString, "Field parameter value incorrect"); }), TestCase("succeeds when a parameter is not added", () => { @@ -113,9 +176,9 @@ public static class PostgresCSharpTests 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.isTrue(it[0].Item2.IsString, "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"); + Expect.isTrue(it[1].Item2.IsString, "Maximum field parameter value incorrect"); }) ]), #pragma warning restore CS0618 @@ -172,7 +235,7 @@ public static class PostgresCSharpTests { Expect.equal( Postgres.Query.WhereByFields(FieldMatch.Any, - [Field.GT("theField", 0).WithParameterName("@test")]), + [Field.GT("theField", "0").WithParameterName("@test")]), "data->>'theField' > @test", "WHERE clause not correct"); }), TestCase("succeeds for a single field when an existence operator is passed", () => @@ -206,7 +269,8 @@ public static class PostgresCSharpTests Expect.equal( Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]), - "data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct"); + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0", + "WHERE clause not correct"); }), TestCase("succeeds for all multiple fields with between operators", () => { @@ -217,10 +281,23 @@ public static class PostgresCSharpTests "WHERE clause not correct"); }) ]), - TestCase("WhereById succeeds", () => - { - Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); - }), + TestList("WhereById", + [ + TestCase("succeeds for numeric ID", () => + { + Expect.equal(Postgres.Query.WhereById(18), "(data->>'Id')::numeric = @id", + "WHERE clause not correct"); + }), + TestCase("succeeds for string ID", () => + { + Expect.equal(Postgres.Query.WhereById("18"), "data->>'Id' = @id", "WHERE clause not correct"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + Expect.equal(Postgres.Query.WhereById(new Uri("https://example.com")), "data->>'Id' = @id", + "WHERE clause not correct"); + }), + ]), TestList("Definition", [ TestCase("EnsureTable succeeds", () => diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index fdd9068..ee34cf2 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -11,9 +11,78 @@ open BitBadger.Documents.Tests let unitTests = testList "Unit" [ testList "Parameters" [ - test "idParam succeeds" { - Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly" - } + testList "idParam" [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + test "succeeds for byte ID" { + Expect.equal + (idParam (sbyte 7)) ("@id", Sql.int8 (sbyte 7)) "Byte ID parameter not constructed correctly" + } + test "succeeds for unsigned byte ID" { + Expect.equal + (idParam (byte 7)) + ("@id", Sql.int8 (int8 (byte 7))) + "Unsigned byte ID parameter not constructed correctly" + } + test "succeeds for short ID" { + Expect.equal + (idParam (int16 44)) + ("@id", Sql.int16 (int16 44)) + "Short ID parameter not constructed correctly" + } + test "succeeds for unsigned short ID" { + Expect.equal + (idParam (uint16 64)) + ("@id", Sql.int16 (int16 64)) + "Unsigned short ID parameter not constructed correctly" + } + test "succeeds for integer ID" { + Expect.equal (idParam 88) ("@id", Sql.int 88) "Int ID parameter not constructed correctly" + } + test "succeeds for unsigned integer ID" { + Expect.equal + (idParam (uint 889)) ("@id", Sql.int 889) "Unsigned int ID parameter not constructed correctly" + } + test "succeeds for long ID" { + Expect.equal + (idParam (int64 123)) + ("@id", Sql.int64 (int64 123)) + "Long ID parameter not constructed correctly" + } + test "succeeds for unsigned long ID" { + Expect.equal + (idParam (uint64 6464)) + ("@id", Sql.int64 (int64 6464)) + "Unsigned long ID parameter not constructed correctly" + } + test "succeeds for decimal ID" { + Expect.equal + (idParam (decimal 4.56)) + ("@id", Sql.decimal (decimal 4.56)) + "Decimal ID parameter not constructed correctly" + } + test "succeeds for single ID" { + Expect.equal + (idParam (single 5.67)) + ("@id", Sql.double (double (single 5.67))) + "Single ID parameter not constructed correctly" + } + test "succeeds for double ID" { + Expect.equal + (idParam (double 6.78)) + ("@id", Sql.double (double 6.78)) + "Double ID parameter not constructed correctly" + } + test "succeeds for string ID" { + Expect.equal (idParam "99") ("@id", Sql.string "99") "String ID parameter not constructed correctly" + } + test "succeeds for non-numeric non-string ID" { + let target = { new obj() with override _.ToString() = "ToString was called" } + Expect.equal + (idParam target) + ("@id", Sql.string "ToString was called") + "Non-numeric, non-string parameter not constructed correctly" + } + ] test "jsonParam succeeds" { Expect.equal (jsonParam "@test" {| Something = "good" |}) @@ -24,35 +93,20 @@ let unitTests = test "succeeds when a parameter is added" { let paramList = addFieldParams [ Field.EQ "it" "242" ] [] Expect.hasLength paramList 1 "There should have been a parameter added" - let it = Seq.head paramList - Expect.equal (fst it) "@field0" "Field parameter name not correct" - match snd it with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@field0" "Parameter name not correct" - Expect.equal value.Value "242" "Parameter value not correct" - | _ -> Expect.isTrue false "The parameter was not a Parameter type" + let name, value = Seq.head paramList + Expect.equal name "@field0" "Field parameter name not correct" + Expect.equal value (Sql.string "242") "Parameter value not correct" } test "succeeds when multiple independent parameters are added" { let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ] Expect.hasLength paramList 3 "There should have been 2 parameters added" let p = Array.ofSeq paramList Expect.equal (fst p[0]) "@id" "First field parameter name not correct" - match snd p[0] with - | SqlValue.String value -> - Expect.equal value "14" "First parameter value not correct" - | _ -> Expect.isTrue false "First parameter was not a String type" + Expect.equal (snd p[0]) (Sql.int 14) "First parameter value not correct" Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct" - match snd p[1] with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@field0" "Second parameter name not correct" - Expect.equal value.Value "you" "Second parameter value not correct" - | _ -> Expect.isTrue false "Second parameter was not a Parameter type" + Expect.equal (snd p[1]) (Sql.string "you") "Second parameter value not correct" Expect.equal (fst p[2]) "@field1" "Third parameter name not correct" - match snd p[2] with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@field1" "Third parameter name not correct" - Expect.equal value.Value "them" "Third parameter value not correct" - | _ -> Expect.isTrue false "Third parameter was not a Parameter type" + Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct" } test "succeeds when a parameter is not added" { let paramList = addFieldParams [ Field.EX "tacos" ] [] @@ -62,33 +116,21 @@ let unitTests = let paramList = addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] [] Expect.hasLength paramList 2 "There should have been 2 parameters added" - let min = Seq.head paramList - Expect.equal (fst min) "@testmin" "Minimum field name not correct" - match snd min with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@testmin" "Minimum parameter name not correct" - Expect.equal value.Value "eh" "Minimum parameter value not correct" - | _ -> Expect.isTrue false "Minimum parameter was not a Parameter type" - let max = paramList |> Seq.skip 1 |> Seq.head - Expect.equal (fst max) "@testmax" "Maximum field name not correct" - match snd max with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@testmax" "Maximum parameter name not correct" - Expect.equal value.Value "zed" "Maximum parameter value not correct" - | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" + let name, value = Seq.head paramList + Expect.equal name "@testmin" "Minimum field name not correct" + Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" + let name, value = paramList |> Seq.skip 1 |> Seq.head + Expect.equal name "@testmax" "Maximum field name not correct" + Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" } ] testList "addFieldParam" [ test "succeeds when a parameter is added" { let paramList = addFieldParam "@field" (Field.EQ "it" "242") [] Expect.hasLength paramList 1 "There should have been a parameter added" - let it = Seq.head paramList - Expect.equal (fst it) "@field" "Field parameter name not correct" - match snd it with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@field" "Parameter name not correct" - Expect.equal value.Value "242" "Parameter value not correct" - | _ -> Expect.isTrue false "The parameter was not a Parameter type" + let name, value = Seq.head paramList + Expect.equal name "@field" "Field parameter name not correct" + Expect.equal value (Sql.string "242") "Parameter value not correct" } test "succeeds when a parameter is not added" { let paramList = addFieldParam "@field" (Field.EX "tacos") [] @@ -97,54 +139,36 @@ let unitTests = test "succeeds when two parameters are added" { let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") [] Expect.hasLength paramList 2 "There should have been 2 parameters added" - let min = Seq.head paramList - Expect.equal (fst min) "@fieldmin" "Minimum field name not correct" - match snd min with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@fieldmin" "Minimum parameter name not correct" - Expect.equal value.Value "eh" "Minimum parameter value not correct" - | _ -> Expect.isTrue false "Minimum parameter was not a Parameter type" - let max = paramList |> Seq.skip 1 |> Seq.head - Expect.equal (fst max) "@fieldmax" "Maximum field name not correct" - match snd max with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@fieldmax" "Maximum parameter name not correct" - Expect.equal value.Value "zed" "Maximum parameter value not correct" - | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" + let name, value = Seq.head paramList + Expect.equal name "@fieldmin" "Minimum field name not correct" + Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" + let name, value = paramList |> Seq.skip 1 |> Seq.head + Expect.equal name "@fieldmax" "Maximum field name not correct" + Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" } ] testList "fieldNameParams" [ test "succeeds for one name" { let name, value = fieldNameParams [ "bob" ] Expect.equal name "@name" "The parameter name was incorrect" - match value with - | SqlValue.String it -> Expect.equal it "bob" "The parameter value was incorrect" - | _ -> Expect.isTrue false "The parameter was not a String type" + Expect.equal value (Sql.string "bob") "The parameter value was incorrect" } test "succeeds for multiple names" { let name, value = fieldNameParams [ "bob"; "tom"; "mike" ] Expect.equal name "@name" "The parameter name was incorrect" - match value with - | SqlValue.StringArray it -> - Expect.equal it [| "bob"; "tom"; "mike" |] "The parameter value was incorrect" - | _ -> Expect.isTrue false "The parameter was not a StringArray type" + Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" } ] testList "fieldNameParam" [ test "succeeds for one name" { let name, value = fieldNameParam [ "bob" ] Expect.equal name "@name" "The parameter name was incorrect" - match value with - | SqlValue.String it -> Expect.equal it "bob" "The parameter value was incorrect" - | _ -> Expect.isTrue false "The parameter was not a String type" + Expect.equal value (Sql.string "bob") "The parameter value was incorrect" } test "succeeds for multiple names" { let name, value = fieldNameParam [ "bob"; "tom"; "mike" ] Expect.equal name "@name" "The parameter name was incorrect" - match value with - | SqlValue.StringArray it -> - Expect.equal it [| "bob"; "tom"; "mike" |] "The parameter value was incorrect" - | _ -> Expect.isTrue false "The parameter was not a StringArray type" + Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" } ] test "noParams succeeds" { @@ -155,7 +179,7 @@ let unitTests = testList "whereByFields" [ test "succeeds for a single field when a logical operator is passed" { Expect.equal - (Query.whereByFields Any [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ]) + (Query.whereByFields Any [ { Field.GT "theField" "0" with ParameterName = Some "@test" } ]) "data->>'theField' > @test" "WHERE clause not correct" } @@ -186,7 +210,7 @@ let unitTests = test "succeeds for any multiple fields with an existence operator" { Expect.equal (Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) - "data->>'thatField' IS NULL OR data->>'thisField' >= @field0" + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0" "WHERE clause not correct" } test "succeeds for all multiple fields with between operators" { @@ -196,9 +220,20 @@ let unitTests = "WHERE clause not correct" } ] - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" - } + testList "whereById" [ + test "succeeds for numeric ID" { + Expect.equal (Query.whereById 18) "(data->>'Id')::numeric = @id" "WHERE clause not correct" + } + test "succeeds for string ID" { + Expect.equal (Query.whereById "18") "data->>'Id' = @id" "WHERE clause not correct" + } + test "succeeds for non-numeric non-string ID" { + Expect.equal + (Query.whereById (System.Uri "https://example.com")) + "data->>'Id' = @id" + "WHERE clause not correct" + } + ] testList "Definition" [ test "ensureTable succeeds" { Expect.equal @@ -505,7 +540,7 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - ftestList "byFields" [ + testList "byFields" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -621,7 +656,7 @@ let integrationTests = PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] Expect.equal (List.length docs) 1 "There should have been one document returned" } - ftestTask "succeeds when documents are not found" { + testTask "succeeds when documents are not found" { use db = PostgresDb.BuildDb() do! loadDocs () -- 2.45.1 From 77a6aaa583c567fa2f5c04ff10ed37c3cf5822f9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 11 Aug 2024 12:10:07 -0400 Subject: [PATCH 15/29] Ignore IDE settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8a30d25..68e3c10 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +**/.idea -- 2.45.1 From 0c308c5f33298c1e5492c0e9339a2eac55d613c9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 11 Aug 2024 17:05:58 -0400 Subject: [PATCH 16/29] Add ToString for FieldMatch; add tests --- src/Common/Library.fs | 4 ++ src/Postgres/Library.fs | 8 ++-- src/Sqlite/Library.fs | 4 +- src/Tests.CSharp/CommonCSharpTests.cs | 34 +++++++++++++++ src/Tests.CSharp/PostgresCSharpTests.cs | 56 +++++++++++++------------ src/Tests.CSharp/SqliteCSharpTests.cs | 21 ++++++++++ src/Tests/CommonTests.fs | 14 +++++-- src/Tests/PostgresTests.fs | 51 ++++++++++++---------- src/Tests/SqliteTests.fs | 21 ++++++++++ 9 files changed, 155 insertions(+), 58 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 4ee5c4f..5bf343a 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -125,6 +125,10 @@ type FieldMatch = | Any /// All fields match (AND) | All + + /// The SQL value implementing each matching strategy + override this.ToString() = + match this with Any -> "OR" | All -> "AND" /// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 365be45..5d7f0c8 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -46,7 +46,7 @@ module private Helpers = () } - /// Create a numerically-typed parameter, or use the given parameter derivation function if non-(numeric or string) + /// Create a number or string parameter, or use the given parameter derivation function if non-(numeric or string) let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) = match box value with | :? int8 as it -> Sql.int8 it @@ -70,7 +70,7 @@ open BitBadger.Documents [] module Parameters = - /// Create an ID parameter (name "@id", key will be treated as a string) + /// Create an ID parameter (name "@id") [] let idParam (key: 'TKey) = "@id", parameterFor key (fun it -> Sql.string (string it)) @@ -134,7 +134,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document [] - let whereByFields howMatched fields = + let whereByFields (howMatched: FieldMatch) fields = let name = ParameterName() let isNumeric (it: obj) = match it with @@ -154,7 +154,7 @@ module Query = if isNumeric value then $"({it.Path PostgreSQL})::numeric {it.Op} {param}" else $"{it.Path PostgreSQL} {it.Op} {param}") - |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") + |> String.concat $" {howMatched} " /// Create a WHERE clause fragment to implement an ID-based query [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 7acc7de..3385788 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -33,7 +33,7 @@ module Query = /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document [] - let whereByFields howMatched fields = + let whereByFields (howMatched: FieldMatch) fields = let name = ParameterName() fields |> Seq.map (fun it -> @@ -43,7 +43,7 @@ module Query = let p = name.Derive it.ParameterName $"{it.Path SQLite} {it.Op} {p}min AND {p}max" | _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}") - |> String.concat (match howMatched with Any -> " OR " | All -> " AND ") + |> String.concat $" {howMatched} " /// Create a WHERE clause fragment to implement an ID-based query [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index fa60bb8..aafa8d6 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -1,6 +1,7 @@ using Expecto.CSharp; using Expecto; using Microsoft.FSharp.Collections; +using Microsoft.FSharp.Core; namespace BitBadger.Documents.Tests.CSharp; @@ -236,6 +237,39 @@ public static class CommonCSharpTests }) ]) ]), + TestList("FieldMatch.ToString", + [ + TestCase("succeeds for Any", () => + { + Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); + }), + TestCase("succeeds for All", () => + { + Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); + }) + ]), + TestList("ParameterName.Derive", + [ + TestCase("succeeds with existing name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Counter should not have advanced for named field"); + }), + TestCase("Derive succeeds with non-existent name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Anonymous field name should have been returned"); + Expect.equal(name.Derive(FSharpOption.None), "@field1", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field2", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field3", + "Counter should have advanced from previous call"); + }) + ]), TestList("Query", [ TestCase("StatementWhere succeeds", () => diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index ff5d08a..5b97370 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -156,32 +156,6 @@ public static class PostgresCSharpTests Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); }) ]), -#pragma warning disable CS0618 - TestList("AddField", - [ - TestCase("succeeds when a parameter is added", () => - { - 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.IsString, "Field parameter value incorrect"); - }), - TestCase("succeeds when a parameter is not added", () => - { - 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"), []).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.IsString, "Minimum field parameter value incorrect"); - Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct"); - Expect.isTrue(it[1].Item2.IsString, "Maximum field parameter value incorrect"); - }) - ]), -#pragma warning restore CS0618 TestList("FieldNames", [ TestCase("succeeds for one name", () => @@ -331,6 +305,36 @@ public static class PostgresCSharpTests { Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", "WHERE clause not correct"); + }), + TestCase("Patch succeeds", () => + { + Expect.equal(Postgres.Query.Patch(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Postgres.Query.RemoveFields(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data - @name", "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Postgres.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", + "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct"); + }), + TestCase ("ByContains succeeds", () => + { + Expect.equal(Postgres.Query.ByContains("exam"), "exam WHERE data @> @criteria", + "By-Contains query not correct"); + }), + TestCase("ByPathMach succeeds", () => + { + Expect.equal(Postgres.Query.ByPathMatch("verify"), "verify WHERE data @? @path::jsonpath", + "By-JSON Path query not correct"); }) ]) ]); diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 60bfd09..be4a89a 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -67,6 +67,27 @@ public static class SqliteCSharpTests { Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); }), + TestCase("Patch succeeds", () => + { + Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName), + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]), + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)", + "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", + "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE data->>'That' > @field0", "By-Field query not correct"); + }), TestCase("Definition.EnsureTable succeeds", () => { Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index edc5cb1..815600a 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -155,13 +155,21 @@ let all = } ] ] - testList "ParameterName" [ - test "Derive succeeds with existing name" { + testList "FieldMatch.ToString" [ + test "succeeds for Any" { + Expect.equal (string Any) "OR" "SQL for Any is incorrect" + } + test "succeeds for All" { + Expect.equal (string All) "AND" "SQL for All is incorrect" + } + ] + testList "ParameterName.Derive" [ + test "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" { + test "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" diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index ee34cf2..3d7fb76 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -124,29 +124,6 @@ let unitTests = Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" } ] - testList "addFieldParam" [ - test "succeeds when a parameter is added" { - let paramList = addFieldParam "@field" (Field.EQ "it" "242") [] - Expect.hasLength paramList 1 "There should have been a parameter added" - let name, value = Seq.head paramList - Expect.equal name "@field" "Field parameter name not correct" - Expect.equal value (Sql.string "242") "Parameter value not correct" - } - test "succeeds when a parameter is not added" { - let paramList = addFieldParam "@field" (Field.EX "tacos") [] - Expect.isEmpty paramList "There should not have been any parameters added" - } - test "succeeds when two parameters are added" { - let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") [] - Expect.hasLength paramList 2 "There should have been 2 parameters added" - let name, value = Seq.head paramList - Expect.equal name "@fieldmin" "Minimum field name not correct" - Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" - let name, value = paramList |> Seq.skip 1 |> Seq.head - Expect.equal name "@fieldmax" "Maximum field name not correct" - Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" - } - ] testList "fieldNameParams" [ test "succeeds for one name" { let name, value = fieldNameParams [ "bob" ] @@ -261,6 +238,34 @@ let unitTests = test "whereJsonPathMatches succeeds" { Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" } + test "patch succeeds" { + Expect.equal + (Query.patch PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE (data->>'That')::numeric > @field0" + "By-Field query not correct" + } + test "byContains succeeds" { + Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct" + } + test "byPathMach succeeds" { + Expect.equal + (Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query not correct" + } ] ] diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 911d5aa..6ca542e 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -55,6 +55,27 @@ let unitTests = test "whereById succeeds" { Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" } + test "patch succeeds" { + Expect.equal + (Query.patch SqliteDb.TableName) + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ]) + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE data->>'That' > @field0" + "By-Field query not correct" + } test "Definition.ensureTable succeeds" { Expect.equal (Query.Definition.ensureTable "tbl") -- 2.45.1 From 96f5e1515da33e6d4eca6aa2982195e4740082d2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 12 Aug 2024 09:07:25 -0400 Subject: [PATCH 17/29] Remove byField functions --- src/Postgres/Library.fs | 96 +------------------- src/Sqlite/Library.fs | 105 +++------------------- src/Tests.CSharp/PostgresCSharpTests.cs | 104 +++++++++++----------- src/Tests.CSharp/SqliteCSharpTests.cs | 98 +++++++++++---------- src/Tests/PostgresTests.fs | 112 +++++++----------------- src/Tests/SqliteTests.fs | 72 +++++++-------- 6 files changed, 186 insertions(+), 401 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 5d7f0c8..d3c5d07 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -426,12 +426,6 @@ module WithProps = fromData<'TDoc> sqlProps - /// Retrieve documents matching a JSON field comparison (->> =) - [] - [] - let byField<'TDoc> tableName field sqlProps = - byFields<'TDoc> tableName Any [ field ] sqlProps - /// Retrieve documents matching JSON field comparisons (->> =) let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) = Custom.List<'TDoc>( @@ -439,11 +433,6 @@ module WithProps = addFieldParams fields [], fromData<'TDoc>, sqlProps) - - /// Retrieve documents matching a JSON field comparison (->> =) - [] - let ByField<'TDoc>(tableName, field, sqlProps) = - ByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) /// Retrieve documents matching a JSON containment query (@>) [] @@ -481,13 +470,7 @@ module WithProps = (addFieldParams fields []) fromData<'TDoc> sqlProps - - /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found - [] - [] - let firstByField<'TDoc> tableName field sqlProps = - firstByFields<'TDoc> tableName Any [ field ] sqlProps - + /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = Custom.Single<'TDoc>( @@ -495,12 +478,7 @@ module WithProps = addFieldParams fields [], fromData<'TDoc>, sqlProps) - - /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found - [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = - FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) - + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = @@ -571,12 +549,6 @@ module WithProps = (Query.byFields (Query.patch tableName) howMatched fields) (addFieldParams fields [ jsonParam "@data" patch ]) sqlProps - - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - [] - let byField tableName field (patch: 'TPatch) sqlProps = - byFields tableName Any [ field ] patch sqlProps /// Patch documents using a JSON containment query in the WHERE clause (@>) [] @@ -611,12 +583,6 @@ module WithProps = (Query.byFields (Query.removeFields tableName) howMatched fields) (addFieldParams fields [ fieldNameParams fieldNames ]) sqlProps - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - [] - let byField tableName field fieldNames sqlProps = - byFields tableName Any [ field ] fieldNames sqlProps /// Remove fields from documents via a JSON containment query (@>) [] @@ -649,12 +615,6 @@ module WithProps = Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) sqlProps - /// Delete documents by matching a JSON field comparison query (->> =) - [] - [] - let byField tableName field sqlProps = - byFields tableName Any [ field ] sqlProps - /// Delete documents by matching a JSON contains query (@>) [] let byContains tableName (criteria: 'TCriteria) sqlProps = @@ -752,12 +712,6 @@ module Count = let byFields tableName howMatched fields = WithProps.Count.byFields tableName howMatched fields (fromDataSource ()) - /// Count matching documents using a JSON field comparison query (->> =) - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - /// Count matching documents using a JSON containment query (@>) [] let byContains tableName criteria = @@ -783,12 +737,6 @@ module Exists = let byFields tableName howMatched fields = WithProps.Exists.byFields tableName howMatched fields (fromDataSource ()) - /// Determine if documents exist using a JSON field comparison query (->> =) - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - /// Determine if documents exist using a JSON containment query (@>) [] let byContains tableName criteria = @@ -827,21 +775,10 @@ module Find = let byFields<'TDoc> tableName howMatched fields = WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ()) - /// Retrieve documents matching a JSON field comparison query (->> =) - [] - [] - let byField<'TDoc> tableName field = - byFields<'TDoc> tableName Any [ field ] - /// Retrieve documents matching a JSON field comparison query (->> =) let ByFields<'TDoc>(tableName, howMatched, fields) = WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) - /// Retrieve documents matching a JSON field comparison query (->> =) - [] - let ByField<'TDoc>(tableName, field) = - ByFields<'TDoc>(tableName, Any, Seq.singleton field) - /// Retrieve documents matching a JSON containment query (@>) [] let byContains<'TDoc> tableName (criteria: obj) = @@ -865,21 +802,10 @@ module Find = let firstByFields<'TDoc> tableName howMatched fields = WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ()) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found - [] - [] - let firstByField<'TDoc> tableName field = - firstByFields<'TDoc> tableName Any [ field ] - /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found - [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = - FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) - /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) = @@ -932,12 +858,6 @@ module Patch = let byFields tableName howMatched fields (patch: 'TPatch) = WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ()) - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - [] - let byField tableName field (patch: 'TPatch) = - byFields tableName Any [ field ] patch - /// Patch documents using a JSON containment query in the WHERE clause (@>) [] let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = @@ -963,12 +883,6 @@ module RemoveFields = let byFields tableName howMatched fields fieldNames = WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ()) - /// Remove fields from documents via a comparison on a JSON field in the document - [] - [] - let byField tableName field fieldNames = - byFields tableName Any [ field ] fieldNames - /// Remove fields from documents via a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) fieldNames = @@ -994,12 +908,6 @@ module Delete = let byFields tableName howMatched fields = WithProps.Delete.byFields tableName howMatched fields (fromDataSource ()) - /// Delete documents by matching a JSON field comparison query (->> =) - [] - [] - let byField tableName field = - byFields tableName Any [ field ] - /// Delete documents by matching a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) = diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 3385788..3ac23de 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -332,12 +332,6 @@ module WithConn = fromData<'TDoc> conn - /// Retrieve documents via a comparison on a JSON field - [] - [] - let byField<'TDoc> tableName field conn = - byFields<'TDoc> tableName Any [ field ] conn - /// Retrieve documents via a comparison on JSON fields let ByFields<'TDoc>(tableName, howMatched, fields, conn) = Custom.List<'TDoc>( @@ -346,11 +340,6 @@ module WithConn = fromData<'TDoc>, conn) - /// Retrieve documents via a comparison on a JSON field - [] - let ByField<'TDoc>(tableName, field, conn) = - ByFields<'TDoc>(tableName, Any, [ field ], conn) - /// Retrieve documents via a comparison on JSON fields, returning only the first result [] let firstByFields<'TDoc> tableName howMatched fields conn = @@ -359,12 +348,6 @@ module WithConn = (addFieldParams fields []) fromData<'TDoc> conn - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - [] - let firstByField<'TDoc> tableName field conn = - firstByFields<'TDoc> tableName Any [ field ] conn /// Retrieve documents via a comparison on JSON fields, returning only the first result let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = @@ -373,11 +356,6 @@ module WithConn = addFieldParams fields [], fromData<'TDoc>, conn) - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = - FirstByFields(tableName, Any, [ field ], conn) /// Commands to update documents [] @@ -417,13 +395,7 @@ module WithConn = (Query.byFields (Query.patch tableName) howMatched fields) (addFieldParams fields [ jsonParam "@data" patch ]) conn - - /// Patch documents using a comparison on a JSON field - [] - [] - let byField tableName field (patch: 'TPatch) conn = - byFields tableName Any [ field ] patch conn - + /// Commands to remove fields from documents [] module RemoveFields = @@ -445,12 +417,6 @@ module WithConn = (Query.byFields (Query.removeFields tableName nameParams) howMatched fields) (addFieldParams fields nameParams) conn - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - [] - let byField tableName field fieldNames conn = - byFields tableName Any [ field ] fieldNames conn /// Commands to delete documents [] @@ -465,12 +431,6 @@ module WithConn = [] let byFields tableName howMatched fields conn = Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn - - /// Delete documents by matching a comparison on a JSON field - [] - [] - let byField tableName field conn = - byFields tableName Any [ field ] conn /// Commands to execute custom SQL queries @@ -516,6 +476,7 @@ module Custom = use conn = Configuration.dbConn () WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) + /// Functions to create tables and indexes [] module Definition = @@ -532,6 +493,7 @@ module Definition = use conn = Configuration.dbConn () WithConn.Definition.ensureFieldIndex tableName indexName fields conn + /// Document insert/save functions [] module Document = @@ -548,6 +510,7 @@ module Document = use conn = Configuration.dbConn () WithConn.save tableName document conn + /// Commands to count documents [] module Count = @@ -563,12 +526,7 @@ module Count = let byFields tableName howMatched fields = use conn = Configuration.dbConn () WithConn.Count.byFields tableName howMatched fields conn - - /// Count matching documents using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] + /// Commands to determine if documents exist [] @@ -585,12 +543,7 @@ module Exists = let byFields tableName howMatched fields = use conn = Configuration.dbConn () WithConn.Exists.byFields tableName howMatched fields conn - - /// Determine if a document exists using a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] + /// Commands to determine if documents exist [] @@ -623,44 +576,23 @@ module Find = let byFields<'TDoc> tableName howMatched fields = use conn = Configuration.dbConn () WithConn.Find.byFields<'TDoc> tableName howMatched fields conn - - /// Retrieve documents via a comparison on a JSON field - [] - [] - let byField<'TDoc> tableName field = - byFields tableName Any [ field ] /// Retrieve documents via a comparison on JSON fields let ByFields<'TDoc>(tableName, howMatched, fields) = use conn = Configuration.dbConn () WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) - - /// Retrieve documents via a comparison on a JSON field - [] - let ByField<'TDoc>(tableName, field) = - ByFields<'TDoc>(tableName, Any, [ field ]) - + /// Retrieve documents via a comparison on JSON fields, returning only the first result [] let firstByFields<'TDoc> tableName howMatched fields = use conn = Configuration.dbConn () WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - [] - let firstByField<'TDoc> tableName field = - firstByFields<'TDoc> tableName Any [ field ] /// Retrieve documents via a comparison on JSON fields, returning only the first result let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = use conn = Configuration.dbConn () WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = - FirstByFields<'TDoc>(tableName, Any, [ field ]) + /// Commands to update documents [] @@ -683,6 +615,7 @@ module Update = use conn = Configuration.dbConn () WithConn.Update.ByFunc(tableName, idFunc, document, conn) + /// Commands to patch (partially update) documents [] module Patch = @@ -698,12 +631,7 @@ module Patch = let byFields tableName howMatched fields (patch: 'TPatch) = use conn = Configuration.dbConn () WithConn.Patch.byFields tableName howMatched fields patch conn - - /// Patch documents using a comparison on a JSON field in the WHERE clause - [] - [] - let byField tableName field (patch: 'TPatch) = - byFields tableName Any [ field ] patch + /// Commands to remove fields from documents [] @@ -720,12 +648,7 @@ module RemoveFields = let byFields tableName howMatched fields fieldNames = use conn = Configuration.dbConn () WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn - - /// Remove field from documents via a comparison on a JSON field in the document - [] - [] - let byField tableName field fieldNames = - byFields tableName Any [ field ] fieldNames + /// Commands to delete documents [] @@ -742,9 +665,3 @@ module Delete = let byFields tableName howMatched fields = use conn = Configuration.dbConn () WithConn.Delete.byFields tableName howMatched fields conn - - /// Delete documents by matching a comparison on a JSON field - [] - [] - let byField tableName field = - byFields tableName Any [ field ] diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 5b97370..3bfa365 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -602,24 +602,27 @@ public static class PostgresCSharpTests var theCount = await Count.All(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), -#pragma warning disable CS0618 - TestCase("ByField succeeds for numeric range", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); + TestList("ByFields", + [ + TestCase("succeeds for numeric range", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); - var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("NumValue", 10, 20)); - Expect.equal(theCount, 3, "There should have been 3 matching documents"); - }), - TestCase("ByField succeeds for non-numeric range", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.BT("NumValue", 10, 20)]); + Expect.equal(theCount, 3, "There should have been 3 matching documents"); + }), + TestCase("succeeds for non-numeric range", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); - var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("Value", "aardvark", "apple")); - Expect.equal(theCount, 1, "There should have been 1 matching document"); - }), -#pragma warning restore CS0618 + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.All, + [Field.BT("Value", "aardvark", "apple")]); + Expect.equal(theCount, 1, "There should have been 1 matching document"); + }) + ]), TestCase("ByContains succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -658,15 +661,14 @@ public static class PostgresCSharpTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var exists = await Exists.ByField(PostgresDb.TableName, Field.NEX("Sub")); + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NEX("Sub")]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when documents do not exist", async () => @@ -674,11 +676,11 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("NumValue", "six")]); Expect.isFalse(exists, "There should not have been existing documents"); }) ]), -#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents exist", async () => @@ -760,15 +762,15 @@ public static class PostgresCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var docs = await Find.ByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.equal(docs.Count, 1, "There should have been one document returned"); }), TestCase("succeeds when documents are not found", async () => @@ -776,11 +778,11 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var docs = await Find.ByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), -#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents are found", async () => @@ -820,15 +822,15 @@ public static class PostgresCSharpTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), -#pragma warning disable CS0618 - TestList("FirstByField", + TestList("FirstByFields", [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc.Id, "two", "The incorrect document was returned"); }), @@ -837,7 +839,8 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); }), @@ -846,11 +849,11 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning restore CS0618 TestList("FirstByContains", [ TestCase("succeeds when a document is found", async () => @@ -999,16 +1002,17 @@ public static class PostgresCSharpTests await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await Count.ByField(PostgresDb.TableName, Field.EQ("NumValue", "77")); + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("NumValue", "77")]); Expect.equal(after, 2, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -1019,10 +1023,10 @@ public static class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) ]), -#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when a document is updated", async () => @@ -1110,15 +1114,14 @@ public static class PostgresCSharpTests await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], new[] { "Sub", "Value" }); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); @@ -1130,7 +1133,8 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Sub" }); + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + new[] { "Sub" }); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -1142,18 +1146,18 @@ public static class PostgresCSharpTests await LoadDocs(); // This not raising an exception is the test - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Nothing" }); + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + new[] { "Nothing" }); }), TestCase("succeeds when no document is matched", async () => { await using var db = PostgresDb.BuildDb(); // This not raising an exception is the test - await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), - new[] { "Value" }); + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.NE("Abracadabra", "apple")], new[] { "Value" }); }) ]), -#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when multiple fields are removed", async () => @@ -1262,15 +1266,14 @@ public static class PostgresCSharpTests Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Delete.ByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 3, "There should have been 3 documents remaining"); }), @@ -1279,12 +1282,11 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Delete.ByField(PostgresDb.TableName, Field.EQ("Value", "crimson")); + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); var remaining = await Count.All(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), -#pragma warning restore CS0618 TestList("ByContains", [ TestCase("succeeds when documents are deleted", async () => diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index be4a89a..e0af7bc 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -355,24 +355,27 @@ public static class SqliteCSharpTests var theCount = await Count.All(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), -#pragma warning disable CS0618 - TestCase("ByField succeeds for numeric range", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + TestList("ByFields", + [ + TestCase("succeeds for numeric range", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("NumValue", 10, 20)); - Expect.equal(theCount, 3L, "There should have been 3 matching documents"); - }), - TestCase("ByField succeeds for non-numeric range", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.BT("NumValue", 10, 20)]); + Expect.equal(theCount, 3L, "There should have been 3 matching documents"); + }), + TestCase("succeeds for non-numeric range", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple")); - Expect.equal(theCount, 1L, "There should have been 1 matching document"); - }) -#pragma warning restore CS0618 + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.BT("Value", "aardvark", "apple")]); + Expect.equal(theCount, 1L, "There should have been 1 matching document"); + }) + ]) ]), TestList("Exists", [ @@ -395,15 +398,14 @@ public static class SqliteCSharpTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents exist", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var exists = await Exists.ByField(SqliteDb.TableName, Field.GE("NumValue", 10)); + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when no matching documents exist", async () => @@ -411,11 +413,11 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Nothing", "none")]); Expect.isFalse(exists, "There should not have been any existing documents"); }) ]) -#pragma warning restore CS0618 ]), TestList("Find", [ @@ -459,15 +461,15 @@ public static class SqliteCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents are found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var docs = await Find.ByField(SqliteDb.TableName, Field.GT("NumValue", 15)); + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)]); Expect.equal(docs.Count, 2, "There should have been two documents returned"); }), TestCase("succeeds when documents are not found", async () => @@ -475,18 +477,20 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var docs = await Find.ByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), - TestList("FirstByField", + TestList("FirstByFields", [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Value", "another")); + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned"); }), @@ -495,7 +499,8 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Sub.Foo", "green")); + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); }), @@ -504,11 +509,11 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) ]) -#pragma warning restore CS0618 ]), TestList("Update", [ @@ -595,16 +600,16 @@ public static class SqliteCSharpTests await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await Count.ByField(SqliteDb.TableName, Field.EQ("NumValue", 77)); + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -615,10 +620,10 @@ public static class SqliteCSharpTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) ]) -#pragma warning restore CS0618 ]), TestList("RemoveFields", [ @@ -651,15 +656,15 @@ public static class SqliteCSharpTests await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when a field is removed", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await RemoveFields.ByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Sub" }); + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + new[] { "Sub" }); var updated = await Find.ById(SqliteDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNull(updated.Sub, "The sub-document should have been removed"); @@ -670,17 +675,18 @@ public static class SqliteCSharpTests await LoadDocs(); // This not raising an exception is the test - await RemoveFields.ByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Nothing" }); + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + new[] { "Nothing" }); }), TestCase("succeeds when no document is matched", async () => { await using var db = await SqliteDb.BuildDb(); // This not raising an exception is the test - await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + new[] { "Value" }); }) ]) -#pragma warning restore CS0618 ]), TestList("Delete", [ @@ -705,15 +711,14 @@ public static class SqliteCSharpTests Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]), -#pragma warning disable CS0618 - TestList("ByField", + TestList("ByFields", [ TestCase("succeeds when documents are deleted", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Delete.ByField(SqliteDb.TableName, Field.NE("Value", "purple")); + await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); }), @@ -722,12 +727,11 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Delete.ByField(SqliteDb.TableName, Field.EQ("Value", "crimson")); + await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.EQ("Value", "crimson")]); var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]) -#pragma warning restore CS0618 ]), TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) ]); diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 3d7fb76..4c4a635 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -497,22 +497,6 @@ let integrationTests = Expect.equal theCount 0 "There should have been no matching documents" } ] - testList "byField" [ - testTask "succeeds for numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20) - Expect.equal theCount 3 "There should have been 3 matching documents" - } - testTask "succeeds for non-numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "Value" "aardvark" "apple") - Expect.equal theCount 1 "There should have been 1 matching document" - } - ] testTask "byContains succeeds" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -562,22 +546,6 @@ let integrationTests = Expect.isFalse exists "There should not have been existing documents" } ] - testList "byField" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField PostgresDb.TableName (Field.EX "Sub") - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when documents do not exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField PostgresDb.TableName (Field.EQ "NumValue" "six") - Expect.isFalse exists "There should not have been existing documents" - } - ] testList "byContains" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() @@ -671,22 +639,6 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] - testList "byField" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField PostgresDb.TableName (Field.EQ "Value" "another") - Expect.equal (List.length docs) 1 "There should have been one document returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField PostgresDb.TableName (Field.EQ "Value" "mauve") - Expect.isEmpty docs "There should have been no documents returned" - } - ] testList "byContains" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() @@ -719,12 +671,12 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] - testList "firstByField" [ + testList "firstByFields" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "another") + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } @@ -732,7 +684,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "purple") + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.isSome doc "There should have been a document returned" Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" } @@ -740,7 +692,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "absent") + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] Expect.isNone doc "There should not have been a document returned" } ] @@ -867,13 +819,13 @@ let integrationTests = do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Patch.byField PostgresDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = Count.byField PostgresDb.TableName (Field.EQ "NumValue" "77") + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" 77 ] Expect.equal after 2 "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -883,7 +835,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Patch.byField PostgresDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] testList "byContains" [ @@ -932,9 +884,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -942,9 +894,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -961,25 +913,25 @@ let integrationTests = do! RemoveFields.byId PostgresDb.TableName "two" [ "Value" ] } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when multiple fields are removed" { use db = PostgresDb.BuildDb() do! loadDocs () - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { use db = PostgresDb.BuildDb() do! loadDocs () - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -987,13 +939,13 @@ let integrationTests = do! loadDocs () // This not raising an exception is the test - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Nothing" ] + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use db = PostgresDb.BuildDb() // This not raising an exception is the test - do! RemoveFields.byField PostgresDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] testList "byContains" [ @@ -1002,9 +954,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -1012,9 +964,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -1037,9 +989,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -1047,9 +999,9 @@ let integrationTests = do! loadDocs () do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -1086,12 +1038,12 @@ let integrationTests = Expect.equal remaining 5 "There should have been 5 documents remaining" } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when documents are deleted" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Delete.byField PostgresDb.TableName (Field.EQ "Value" "purple") + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] let! remaining = Count.all PostgresDb.TableName Expect.equal remaining 3 "There should have been 3 documents remaining" } @@ -1099,7 +1051,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - do! Delete.byField PostgresDb.TableName (Field.EQ "Value" "crimson") + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = Count.all PostgresDb.TableName Expect.equal remaining 5 "There should have been 5 documents remaining" } diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 6ca542e..c6a0ee3 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -332,20 +332,22 @@ let integrationTests = let! theCount = Count.all SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } - testTask "byField succeeds for a numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField SqliteDb.TableName (Field.BT "NumValue" 10 20) - Expect.equal theCount 3L "There should have been 3 matching documents" - } - testTask "byField succeeds for a non-numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField SqliteDb.TableName (Field.BT "Value" "aardvark" "apple") - Expect.equal theCount 1L "There should have been 1 matching document" - } + testList "byFields" [ + testTask "succeeds for a numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "NumValue" 10 20 ] + Expect.equal theCount 3L "There should have been 3 matching documents" + } + testTask "succeeds for a non-numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "Value" "aardvark" "apple" ] + Expect.equal theCount 1L "There should have been 1 matching document" + } + ] ] testList "Exists" [ testList "byId" [ @@ -364,19 +366,19 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when documents exist" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byField SqliteDb.TableName (Field.EQ "NumValue" 10) + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when no matching documents exist" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byField SqliteDb.TableName (Field.LT "Nothing" "none") + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.LT "Nothing" "none" ] Expect.isFalse exists "There should not have been any existing documents" } ] @@ -421,28 +423,28 @@ let integrationTests = Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when documents are found" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! docs = Find.byField SqliteDb.TableName (Field.GT "NumValue" 15) + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] Expect.equal (List.length docs) 2 "There should have been two documents returned" } testTask "succeeds when documents are not found" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! docs = Find.byField SqliteDb.TableName (Field.GT "NumValue" 100) + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 100 ] Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" } ] - testList "firstByField" [ + testList "firstByFields" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Value" "another") + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } @@ -450,7 +452,7 @@ let integrationTests = use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" } @@ -458,7 +460,7 @@ let integrationTests = use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Value" "absent") + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] @@ -535,13 +537,13 @@ let integrationTests = do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! Patch.byField SqliteDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = Count.byField SqliteDb.TableName (Field.EQ "NumValue" 77) + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] Expect.equal after 2L "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -551,7 +553,7 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! Patch.byField SqliteDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] ] @@ -583,12 +585,12 @@ let integrationTests = do! RemoveFields.byId SqliteDb.TableName "two" [ "Value" ] } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when a field is removed" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! RemoveFields.byField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Sub" ] + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] try let! _ = Find.byId SqliteDb.TableName "four" Expect.isTrue false "The updated document should have failed to parse" @@ -601,13 +603,13 @@ let integrationTests = do! loadDocs () // This not raising an exception is the test - do! RemoveFields.byField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Nothing" ] + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use! db = SqliteDb.BuildDb() // This not raising an exception is the test - do! RemoveFields.byField SqliteDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] ] @@ -630,12 +632,12 @@ let integrationTests = Expect.equal remaining 5L "There should have been 5 documents remaining" } ] - testList "byField" [ + testList "byFields" [ testTask "succeeds when documents are deleted" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byField SqliteDb.TableName (Field.NE "Value" "purple") + do! Delete.byFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] let! remaining = Count.all SqliteDb.TableName Expect.equal remaining 2L "There should have been 2 documents remaining" } @@ -643,7 +645,7 @@ let integrationTests = use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byField SqliteDb.TableName (Field.EQ "Value" "crimson") + do! Delete.byFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = Count.all SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } -- 2.45.1 From edd4d006da7edf357064bbac36b1a7bcce0a2fb1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 13 Aug 2024 09:03:05 -0400 Subject: [PATCH 18/29] First cut of field order by function --- src/Common/Library.fs | 12 ++++++++++++ src/Directory.Build.props | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 5bf343a..00d9bb2 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -292,3 +292,15 @@ module Query = [] let selectFromTable tableName = find tableName + + /// Create an ORDER BY clause for the given fields + [] + let orderBy fields dialect = + if Seq.isEmpty fields then "" + else + fields + |> Seq.map (fun it -> + let direction = if it.Name.Contains ' ' then $" {(it.Name.Split ' ')[1]}" else "" + it.Path dialect + direction) + |> String.concat ", " + |> function it -> $" ORDER BY {it}" diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ea70aa3..e73f3da 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,10 @@ net6.0;net8.0 embedded false - 3.1.0.0 - 3.1.0.0 - 3.1.0 - Add BT (between) operator; drop .NET 7 support + 4.0.0.0 + 4.0.0.0 + 4.0.0 + Change ByField to ByFields; support dot-access to document fields; add Find*Ordered functions/methods danieljsummers Bit Badger Solutions README.md -- 2.45.1 From bac3bd2ef09737d66cc0850ed11547c64adc0431 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 14 Aug 2024 22:13:46 -0400 Subject: [PATCH 19/29] Add *Ordered functions - WIP on PostgreSQL numeric ordering --- src/Common/Library.fs | 11 +- src/Postgres/Extensions.fs | 76 +++++++ src/Postgres/Library.fs | 187 ++++++++++++++++++ src/Sqlite/Extensions.fs | 30 +++ src/Sqlite/Library.fs | 80 ++++++++ src/Tests.CSharp/CommonCSharpTests.cs | 70 ++++++- .../PostgresCSharpExtensionTests.cs | 2 +- src/Tests.CSharp/PostgresCSharpTests.cs | 2 +- .../SqliteCSharpExtensionTests.cs | 2 +- src/Tests.CSharp/SqliteCSharpTests.cs | 2 +- src/Tests/BitBadger.Documents.Tests.fsproj | 1 + src/Tests/CommonTests.fs | 56 ++++++ src/Tests/PostgresTests.fs | 24 +++ 13 files changed, 536 insertions(+), 7 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 00d9bb2..8311375 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -105,6 +105,10 @@ type Field = { else $"->>'{name}'" $"data{path}" + /// Create a field with a given name, but no other properties filled (op will be EQ, value will be "") + static member Named name = + { Name = name; Op = EQ; Value = ""; ParameterName = None; Qualifier = None } + /// Specify the name of the parameter for this field member this.WithParameterName name = { this with ParameterName = Some name } @@ -300,7 +304,10 @@ module Query = else fields |> Seq.map (fun it -> - let direction = if it.Name.Contains ' ' then $" {(it.Name.Split ' ')[1]}" else "" - it.Path dialect + direction) + if it.Name.Contains ' ' then + let parts = it.Name.Split ' ' + { it with Name = parts[0] }, Some $" {parts[1]}" + else it, None) + |> Seq.map (fun (field, direction) -> field.Path dialect + defaultArg direction "") |> String.concat ", " |> function it -> $" ORDER BY {it}" diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index 818d11b..6492c1a 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -92,6 +92,10 @@ module Extensions = member conn.findAll<'TDoc> tableName = WithProps.Find.all<'TDoc> tableName (Sql.existingConnection conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + member conn.findAllOrdered<'TDoc> tableName orderFields = + WithProps.Find.allOrdered<'TDoc> tableName orderFields (Sql.existingConnection conn) + /// Retrieve a document by its ID; returns None if not found member conn.findById<'TKey, 'TDoc> tableName docId = WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn) @@ -100,6 +104,12 @@ module Extensions = member conn.findByFields<'TDoc> tableName howMatched fields = WithProps.Find.byFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document + member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.byFieldsOrdered<'TDoc> + tableName howMatched queryFields orderFields (Sql.existingConnection conn) + /// Retrieve documents matching a JSON field comparison query (->> =) [] member conn.findByField<'TDoc> tableName field = @@ -109,14 +119,28 @@ module Extensions = member conn.findByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + member conn.findByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) member conn.findByJsonPath<'TDoc> tableName jsonPath = WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + member conn.findByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found member conn.findFirstByFields<'TDoc> tableName howMatched fields = WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in + /// the document; returns None if not found + member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.firstByFieldsOrdered<'TDoc> + tableName howMatched queryFields orderFields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found [] member conn.findFirstByField<'TDoc> tableName field = @@ -126,10 +150,20 @@ module Extensions = member conn.findFirstByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns None if not found + member conn.findFirstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found member conn.findFirstByJsonPath<'TDoc> tableName jsonPath = WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns None if not found + member conn.findFirstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn) + /// Update an entire document by its ID member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = WithProps.Update.byId tableName docId document (Sql.existingConnection conn) @@ -310,6 +344,11 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindAll<'TDoc>(conn, tableName) = WithProps.Find.All<'TDoc>(tableName, Sql.existingConnection conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) = + WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, Sql.existingConnection conn) + /// Retrieve a document by its ID; returns None if not found [] static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = @@ -320,6 +359,12 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + [] + static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) = + WithProps.Find.ByFieldsOrdered<'TDoc>( + tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) + /// Retrieve documents matching a JSON field comparison query (->> =) [] [] @@ -331,16 +376,34 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) = WithProps.Find.ByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + static member inline FindByContainsOrdered<'TDoc>(conn, tableName, criteria: obj, orderFields) = + WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) [] static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + [] + static member inline FindByJsonPathOrdered<'TDoc>(conn, tableName, jsonPath, orderFields) = + WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns null if not found + [] + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, howMatched, queryFields, orderFields) = + WithProps.Find.FirstByFieldsOrdered<'TDoc>( + tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] [] @@ -352,11 +415,24 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns None if not found + [] + static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, criteria: obj, orderFields) = + WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns None if not found + [] + static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null>(conn, tableName, jsonPath, orderFields) = + WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) + /// Update an entire document by its ID [] static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) = diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index d3c5d07..b5abc7d 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -407,6 +407,16 @@ module WithProps = let All<'TDoc>(tableName, sqlProps) = Custom.List<'TDoc>(Query.find tableName, [], fromData<'TDoc>, sqlProps) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields sqlProps = + Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields PostgreSQL) [] fromData<'TDoc> sqlProps + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc>(tableName, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.find tableName + Query.orderBy orderFields PostgreSQL, [], fromData<'TDoc>, sqlProps) + /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps = @@ -434,6 +444,23 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve documents matching JSON field comparisons (->> =) ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps = + Custom.list<'TDoc> + (Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL) + (addFieldParams queryFields []) + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching JSON field comparisons (->> =) ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL, + addFieldParams queryFields [], + fromData<'TDoc>, + sqlProps) + /// Retrieve documents matching a JSON containment query (@>) [] let byContains<'TDoc> tableName (criteria: obj) sqlProps = @@ -448,6 +475,23 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps = + Custom.list<'TDoc> + (Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL) + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL, + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + /// Retrieve documents matching a JSON Path match query (@?) [] let byJsonPath<'TDoc> tableName jsonPath sqlProps = @@ -462,6 +506,23 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + [] + let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps = + Custom.list<'TDoc> + (Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL) + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL, + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) + /// Retrieve the first document matching JSON field comparisons (->> =); returns None if not found [] let firstByFields<'TDoc> tableName howMatched fields sqlProps = @@ -479,6 +540,25 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + (addFieldParams queryFields []) + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the + /// document; returns null if not found + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1", + addFieldParams queryFields [], + fromData<'TDoc>, + sqlProps) + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = @@ -496,6 +576,25 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns null if not found + let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] let firstByJsonPath<'TDoc> tableName jsonPath sqlProps = @@ -513,6 +612,25 @@ module WithProps = fromData<'TDoc>, sqlProps) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns null if not found + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) + /// Commands to update documents [] module Update = @@ -761,6 +879,15 @@ module Find = let All<'TDoc> tableName = WithProps.Find.All<'TDoc>(tableName, fromDataSource ()) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields = + WithProps.Find.allOrdered<'TDoc> tableName orderFields (fromDataSource ()) + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc> tableName orderFields = + WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, fromDataSource ()) + /// Retrieve a document by its ID; returns None if not found [] let byId<'TKey, 'TDoc> tableName docId = @@ -779,6 +906,15 @@ module Find = let ByFields<'TDoc>(tableName, howMatched, fields) = WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryField orderFields = + WithProps.Find.byFieldsOrdered<'TDoc> tableName howMatched queryField orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) = + WithProps.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) + /// Retrieve documents matching a JSON containment query (@>) [] let byContains<'TDoc> tableName (criteria: obj) = @@ -788,6 +924,15 @@ module Find = let ByContains<'TDoc>(tableName, criteria: obj) = WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ()) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields) = + WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) + /// Retrieve documents matching a JSON Path match query (@?) [] let byJsonPath<'TDoc> tableName jsonPath = @@ -797,6 +942,15 @@ module Find = let ByJsonPath<'TDoc>(tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + [] + let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields) = + WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found [] let firstByFields<'TDoc> tableName howMatched fields = @@ -806,6 +960,17 @@ module Find = let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns null if not found + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) = @@ -815,6 +980,17 @@ module Find = let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns None if not found + [] + let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns null if not found + let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields) = + WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] let firstByJsonPath<'TDoc> tableName jsonPath = @@ -824,6 +1000,17 @@ module Find = let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns None if not found + [] + let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns null if not found + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields) = + WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) + /// Commands to update documents [] diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 576b771..c5e83e5 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -71,6 +71,10 @@ module Extensions = member conn.findAll<'TDoc> tableName = WithConn.Find.all<'TDoc> tableName conn + /// Retrieve all documents in the given table ordered by the given fields in the document + member conn.findAllOrdered<'TDoc> tableName orderFields = + WithConn.Find.allOrdered<'TDoc> tableName orderFields conn + /// Retrieve a document by its ID member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) = WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn @@ -79,6 +83,10 @@ module Extensions = member conn.findByFields<'TDoc> tableName howMatched fields = WithConn.Find.byFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + /// Retrieve documents via a comparison on a JSON field [] member conn.findByField<'TDoc> tableName field = @@ -88,6 +96,11 @@ module Extensions = member conn.findFirstByFields<'TDoc> tableName howMatched fields = WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + /// Retrieve documents via a comparison on a JSON field, returning only the first result [] member conn.findFirstByField<'TDoc> tableName field = @@ -225,6 +238,11 @@ type SqliteConnectionCSharpExtensions = static member inline FindAll<'TDoc>(conn, tableName) = WithConn.Find.All<'TDoc>(tableName, conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) = + WithConn.Find.AllOrdered<'TDoc>(tableName, conn, orderFields) + /// Retrieve a document by its ID [] static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = @@ -235,6 +253,11 @@ type SqliteConnectionCSharpExtensions = static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + [] + static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) = + WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + /// Retrieve documents via a comparison on a JSON field [] [] @@ -246,6 +269,13 @@ type SqliteConnectionCSharpExtensions = static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + [] + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, howMatched, queryFields, orderFields) = + WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + /// Retrieve documents via a comparison on a JSON field, returning only the first result [] [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 3ac23de..7fb3afd 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -314,6 +314,15 @@ module WithConn = let All<'TDoc>(tableName, conn) = Custom.List(Query.find tableName, [], fromData<'TDoc>, conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields conn = + Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields SQLite) [] fromData<'TDoc> conn + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc>(tableName, orderFields, conn) = + Custom.List(Query.find tableName + Query.orderBy orderFields PostgreSQL, [], fromData<'TDoc>, conn) + /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn = @@ -340,6 +349,23 @@ module WithConn = fromData<'TDoc>, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn = + Custom.list<'TDoc> + (Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite) + (addFieldParams queryFields []) + fromData<'TDoc> + conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) = + Custom.List<'TDoc>( + Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite, + addFieldParams queryFields [], + fromData<'TDoc>, + conn) + /// Retrieve documents via a comparison on JSON fields, returning only the first result [] let firstByFields<'TDoc> tableName howMatched fields conn = @@ -357,6 +383,25 @@ module WithConn = fromData<'TDoc>, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn = + Custom.single + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1" + (addFieldParams queryFields []) + fromData<'TDoc> + conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, conn) = + Custom.Single( + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1", + addFieldParams queryFields [], + fromData<'TDoc>, + conn) + /// Commands to update documents [] module Update = @@ -560,6 +605,17 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.All<'TDoc>(tableName, conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields = + use conn = Configuration.dbConn () + WithConn.Find.allOrdered<'TDoc> tableName orderFields conn + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc> tableName orderFields = + use conn = Configuration.dbConn () + WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn) + /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName docId = @@ -582,6 +638,17 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + use conn = Configuration.dbConn () + WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) = + use conn = Configuration.dbConn () + WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + /// Retrieve documents via a comparison on JSON fields, returning only the first result [] let firstByFields<'TDoc> tableName howMatched fields = @@ -593,6 +660,19 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + use conn = Configuration.dbConn () + WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + use conn = Configuration.dbConn () + WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + /// Commands to update documents [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index aafa8d6..547e300 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -174,6 +174,31 @@ public static class CommonCSharpTests Expect.equal(field.Name, "Rad", "Field name incorrect"); Expect.equal(field.Op, Op.NEX, "Operator incorrect"); }), + TestList("NameToPath", + [ + TestCase("succeeds for PostgreSQL and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), + "Path not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL and a nested name", () => + { + Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a nested name", () => + { + Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), + "Path not constructed correctly"); + }) + ]), TestCase("WithParameterName succeeds", () => { var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); @@ -356,7 +381,50 @@ public static class CommonCSharpTests TestCase("Delete succeeds", () => { Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); - }) + }), + TestList("OrderBy", + [ + TestCase("succeeds for no fields", () => + { + Expect.equal("", Query.OrderBy([], Dialect.PostgreSQL), + "Order By should have been blank (PostgreSQL)"); + Expect.equal("", Query.OrderBy([], Dialect.SQLite), "Order By should have been blank (SQLite)"); + }), + TestCase("succeeds for PostgreSQL with one field and no direction", () => + { + Expect.equal(" ORDER BY data->>'TestField'", + Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), + "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with one field and no direction", () => + { + Expect.equal(" ORDER BY data->>'TestField'", + Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), + "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL with multiple fields and direction", () => + { + Expect.equal(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], + Dialect.PostgreSQL), "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with multiple fields and direction", () => + { + Expect.equal( + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], + Dialect.SQLite), + "Order By not constructed correctly"); + }) + ]) ]) ]); } diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs index df85d27..d23f6d3 100644 --- a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -41,7 +41,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.CustomList(Query.SelectFromTable(PostgresDb.TableName), Parameters.None, + var docs = await conn.CustomList(Query.Find(PostgresDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 3bfa365..1f53ba7 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -400,7 +400,7 @@ public static class PostgresCSharpTests await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var docs = await Custom.List(Query.SelectFromTable(PostgresDb.TableName), Parameters.None, + var docs = await Custom.List(Query.Find(PostgresDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 87ceeea..70098ce 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -52,7 +52,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.CustomList(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + var docs = await conn.CustomList(Query.Find(SqliteDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index e0af7bc..e0c534e 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -194,7 +194,7 @@ public static class SqliteCSharpTests await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var docs = await Custom.List(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + var docs = await Custom.List(Query.Find(SqliteDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index 90c8b65..d976fb3 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -2,6 +2,7 @@ Exe + 1182 diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 815600a..4b3b718 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -109,6 +109,28 @@ let all = Expect.isNone field.ParameterName "The default parameter name should be None" Expect.isNone field.Qualifier "The default table qualifier should be None" } + testList "NameToPath" [ + test "succeeds for PostgreSQL and a simple name" { + Expect.equal + "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly" + } + test "succeeds for SQLite and a simple name" { + Expect.equal + "data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly" + } + test "succeeds for PostgreSQL and a nested name" { + Expect.equal + "data#>>'{A,Long,Path,to,the,Property}'" + (Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL) + "Path not constructed correctly" + } + test "succeeds for SQLite and a nested name" { + Expect.equal + "data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'" + (Field.NameToPath "A.Long.Path.to.the.Property" SQLite) + "Path not constructed correctly" + } + ] test "WithParameterName succeeds" { let field = (Field.EQ "Bob" "Tom").WithParameterName "@name" Expect.isSome field.ParameterName "The parameter name should have been filled" @@ -253,5 +275,39 @@ let all = test "delete succeeds" { Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct" } + testList "orderBy" [ + test "succeeds for no fields" { + Expect.equal "" (Query.orderBy [] PostgreSQL) "Order By should have been blank (PostgreSQL)" + Expect.equal "" (Query.orderBy [] SQLite) "Order By should have been blank (SQLite)" + } + test "succeeds for PostgreSQL with one field and no direction" { + Expect.equal + " ORDER BY data->>'TestField'" + (Query.orderBy [ Field.Named "TestField" ] PostgreSQL) + "Order By not constructed correctly" + } + test "succeeds for SQLite with one field and no direction" { + Expect.equal + " ORDER BY data->>'TestField'" + (Query.orderBy [ Field.Named "TestField" ] SQLite) + "Order By not constructed correctly" + } + test "succeeds for PostgreSQL with multiple fields and direction" { + Expect.equal + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" + (Query.orderBy + [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] + PostgreSQL) + "Order By not constructed correctly" + } + test "succeeds for SQLite with multiple fields and direction" { + Expect.equal + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" + (Query.orderBy + [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] + SQLite) + "Order By not constructed correctly" + } + ] ] ] diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 4c4a635..b930aeb 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -602,6 +602,30 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] + testList "allOrdered" [ + ptestTask "succeeds when ordering numerically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.EQ "NumValue" 0 ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "byId" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() -- 2.45.1 From e07844570a7881c4a6aa0da95639147a8c160d81 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 15 Aug 2024 19:16:00 -0400 Subject: [PATCH 20/29] Fix numeric field rendering in order by --- src/Common/Library.fs | 7 ++++++- src/Tests/PostgresTests.fs | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 8311375..fd2554b 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -308,6 +308,11 @@ module Query = let parts = it.Name.Split ' ' { it with Name = parts[0] }, Some $" {parts[1]}" else it, None) - |> Seq.map (fun (field, direction) -> field.Path dialect + defaultArg direction "") + |> Seq.map (fun (field, direction) -> + let path = + if dialect = PostgreSQL && field.Name.StartsWith "n:" then + $"({ { field with Name = field.Name[2..] }.Path dialect})::numeric" + else field.Path dialect + path + defaultArg direction "") |> String.concat ", " |> function it -> $" ORDER BY {it}" diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index b930aeb..0a2e3ba 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -602,12 +602,12 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] - testList "allOrdered" [ - ptestTask "succeeds when ordering numerically" { + ftestList "allOrdered" [ + testTask "succeeds when ordering numerically" { use db = PostgresDb.BuildDb() do! loadDocs () - let! results = Find.allOrdered PostgresDb.TableName [ Field.EQ "NumValue" 0 ] + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] Expect.hasLength results 5 "There should have been 5 documents returned" Expect.equal (results |> List.map _.Id |> String.concat "|") -- 2.45.1 From 8e33299e220df7e83a7ae9e4333b138cfa082c60 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 15 Aug 2024 22:32:25 -0400 Subject: [PATCH 21/29] WIP on ordered tests, test reorg --- src/Tests.CSharp/PostgresCSharpTests.cs | 2742 ++++++++++++----------- src/Tests/PostgresTests.fs | 2341 ++++++++++--------- 2 files changed, 2702 insertions(+), 2381 deletions(-) diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 1f53ba7..75335ae 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -13,330 +13,333 @@ using static Runner; /// public static class PostgresCSharpTests { + /// + /// Unit tests for the Parameters module of the PostgreSQL library + /// + private static readonly Test ParametersTests = TestList("Parameters", + [ + TestList("Id", + [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + TestCase("succeeds for byte ID", () => + { + var it = Parameters.Id((sbyte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned byte ID", () => + { + var it = Parameters.Id((byte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Unsigned byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for short ID", () => + { + var it = Parameters.Id((short)44); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(44), "Short ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned short ID", () => + { + var it = Parameters.Id((ushort)64); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(64), "Unsigned short ID parameter not constructed correctly"); + }), + TestCase("succeeds for integer ID", () => + { + var it = Parameters.Id(88); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@int(88), "ID parameter value incorrect"); + }), + TestCase("succeeds for unsigned integer ID", () => + { + var it = Parameters.Id((uint)889); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@int(889), "Unsigned int ID parameter not constructed correctly"); + }), + TestCase("succeeds for long ID", () => + { + var it = Parameters.Id((long)123); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(123), "Long ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned long ID", () => + { + var it = Parameters.Id((ulong)6464); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(6464), "Unsigned long ID parameter not constructed correctly"); + }), + TestCase("succeeds for decimal ID", () => + { + var it = Parameters.Id((decimal)4.56); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@decimal((decimal)4.56), "Decimal ID parameter not constructed correctly"); + }), + TestCase("succeeds for single ID", () => + { + var it = Parameters.Id((float)5.67); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double((float)5.67), "Single ID parameter not constructed correctly"); + }), + TestCase("succeeds for double ID", () => + { + var it = Parameters.Id(6.78); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double(6.78), "Double ID parameter not constructed correctly"); + }), + TestCase("succeeds for string ID", () => + { + var it = Parameters.Id("99"); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("99"), "ID parameter value incorrect"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + var it = Parameters.Id(new Uri("https://example.com")); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("https://example.com/"), + "Non-numeric, non-string parameter value incorrect"); + }) + ]), + TestCase("Json succeeds", () => + { + var it = Parameters.Json("@test", new { Something = "good" }); + Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); + }), + TestList("AddFields", + [ + TestCase("succeeds when a parameter is added", () => + { + var paramList = Parameters.AddFields([Field.EQ("it", "242")], []).ToList(); + Expect.hasLength(paramList, 1, "There should have been a parameter added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@field0", "Field parameter name not correct"); + Expect.equal(value, Sql.@string("242"), "Field parameter value not correct"); + }), + TestCase("succeeds when multiple independent parameters are added", () => + { + var paramList = Parameters.AddFields([Field.EQ("me", "you"), Field.GT("us", "them")], + [Parameters.Id(14)]).ToList(); + Expect.hasLength(paramList, 3, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@id", "First field parameter name not correct"); + Expect.equal(value, Sql.@int(14), "First field parameter value not correct"); + (name, value) = paramList[1]; + Expect.equal(name, "@field0", "Second field parameter name not correct"); + Expect.equal(value, Sql.@string("you"), "Second field parameter value not correct"); + (name, value) = paramList[2]; + Expect.equal(name, "@field1", "Third parameter name not correct"); + Expect.equal(value, Sql.@string("them"), "Third parameter value not correct"); + }), + TestCase("succeeds when a parameter is not added", () => + { + var paramList = Parameters.AddFields([Field.EX("tacos")], []).ToList(); + Expect.isEmpty(paramList, "There should not have been any parameters added"); + }), + TestCase("succeeds when two parameters are added for one field", () => + { + var paramList = + Parameters.AddFields([Field.BT("that", "eh", "zed").WithParameterName("@test")], []).ToList(); + Expect.hasLength(paramList, 2, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@testmin", "Minimum field name not correct"); + Expect.equal(value, Sql.@string("eh"), "Minimum field value not correct"); + (name, value) = paramList[1]; + Expect.equal(name, "@testmax", "Maximum field name not correct"); + Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); + }) + ]), + TestList("FieldNames", + [ + TestCase("succeeds for one name", () => + { + var (name, value) = Parameters.FieldNames(["bob"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsString) + { + Expect.isTrue(false, "The parameter was not a String type"); + } + }), + TestCase("succeeds for multiple names", () => + { + var (name, value) = Parameters.FieldNames(["bob", "tom", "mike"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsStringArray) + { + Expect.isTrue(false, "The parameter was not a StringArray type"); + } + }) + ]), +#pragma warning disable CS0618 + TestList("FieldName", + [ + TestCase("succeeds for one name", () => + { + var (name, value) = Parameters.FieldName(["bob"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsString) + { + Expect.isTrue(false, "The parameter was not a String type"); + } + }), + TestCase("succeeds for multiple names", () => + { + var (name, value) = Parameters.FieldName(["bob", "tom", "mike"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsStringArray) + { + Expect.isTrue(false, "The parameter was not a StringArray type"); + } + }) + ]) +#pragma warning restore CS0618 + ]); + + /// + /// Unit tests for the Query module of the PostgreSQL library + /// + private static readonly Test QueryTests = TestList("Query", + [ + TestList("WhereByFields", + [ + TestCase("succeeds for a single field when a logical operator is passed", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.GT("theField", "0").WithParameterName("@test")]), + "data->>'theField' > @test", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when an existence operator is passed", () => + { + Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), + "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(FieldMatch.All, + [Field.BT("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", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.BT("field0", "a", "b").WithParameterName("@alpha")]), + "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with logical operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), + "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(FieldMatch.Any, + [Field.NEX("thatField"), Field.GE("thisField", 18)]), + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0", + "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with between operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); + }) + ]), + TestList("WhereById", + [ + TestCase("succeeds for numeric ID", () => + { + Expect.equal(Postgres.Query.WhereById(18), "(data->>'Id')::numeric = @id", "WHERE clause not correct"); + }), + TestCase("succeeds for string ID", () => + { + Expect.equal(Postgres.Query.WhereById("18"), "data->>'Id' = @id", "WHERE clause not correct"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + Expect.equal(Postgres.Query.WhereById(new Uri("https://example.com")), "data->>'Id' = @id", + "WHERE clause not correct"); + }), + ]), + TestList("Definition", + [ + TestCase("EnsureTable succeeds", () => + { + Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)", + "CREATE TABLE statement not constructed correctly"); + }), + TestCase("EnsureDocumentIndex succeeds for full index", () => + { + Expect.equal(Postgres.Query.Definition.EnsureDocumentIndex("schema.tbl", DocumentIndex.Full), + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)", + "CREATE INDEX statement not constructed correctly"); + }), + TestCase("EnsureDocumentIndex succeeds for JSONB Path Ops index", () => + { + Expect.equal( + Postgres.Query.Definition.EnsureDocumentIndex(PostgresDb.TableName, DocumentIndex.Optimized), + string.Format( + "CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)", + PostgresDb.TableName), + "CREATE INDEX statement not constructed correctly"); + }) + ]), + TestCase("WhereDataContains succeeds", () => + { + Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", "WHERE clause not correct"); + }), + TestCase("WhereJsonPathMatches succeeds", () => + { + Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", + "WHERE clause not correct"); + }), + TestCase("Patch succeeds", () => + { + Expect.equal(Postgres.Query.Patch(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Postgres.Query.RemoveFields(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data - @name", "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Postgres.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct"); + }), + TestCase("ByContains succeeds", () => + { + Expect.equal(Postgres.Query.ByContains("exam"), "exam WHERE data @> @criteria", + "By-Contains query not correct"); + }), + TestCase("ByPathMach succeeds", () => + { + Expect.equal(Postgres.Query.ByPathMatch("verify"), "verify WHERE data @? @path::jsonpath", + "By-JSON Path query not correct"); + }) + ]); + /// /// Tests which do not hit the database /// private static readonly Test Unit = TestList("Unit", [ - TestList("Parameters", - [ - TestList("Id", - [ - // NOTE: these tests also exercise all branches of the internal parameterFor function - TestCase("succeeds for byte ID", () => - { - var it = Parameters.Id((sbyte)7); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int8(7), "Byte ID parameter not constructed correctly"); - }), - TestCase("succeeds for unsigned byte ID", () => - { - var it = Parameters.Id((byte)7); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int8(7), "Unsigned byte ID parameter not constructed correctly"); - }), - TestCase("succeeds for short ID", () => - { - var it = Parameters.Id((short)44); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int16(44), "Short ID parameter not constructed correctly"); - }), - TestCase("succeeds for unsigned short ID", () => - { - var it = Parameters.Id((ushort)64); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int16(64), "Unsigned short ID parameter not constructed correctly"); - }), - TestCase("succeeds for integer ID", () => - { - var it = Parameters.Id(88); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@int(88), "ID parameter value incorrect"); - }), - TestCase("succeeds for unsigned integer ID", () => - { - var it = Parameters.Id((uint)889); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@int(889), "Unsigned int ID parameter not constructed correctly"); - }), - TestCase("succeeds for long ID", () => - { - var it = Parameters.Id((long)123); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int64(123), "Long ID parameter not constructed correctly"); - }), - TestCase("succeeds for unsigned long ID", () => - { - var it = Parameters.Id((ulong)6464); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.int64(6464), - "Unsigned long ID parameter not constructed correctly"); - }), - TestCase("succeeds for decimal ID", () => - { - var it = Parameters.Id((decimal)4.56); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@decimal((decimal)4.56), - "Decimal ID parameter not constructed correctly"); - }), - TestCase("succeeds for single ID", () => - { - var it = Parameters.Id((float)5.67); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@double((float)5.67), - "Single ID parameter not constructed correctly"); - }), - TestCase("succeeds for double ID", () => - { - var it = Parameters.Id(6.78); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@double(6.78), - "Double ID parameter not constructed correctly"); - }), - TestCase("succeeds for string ID", () => - { - var it = Parameters.Id("99"); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@string("99"), "ID parameter value incorrect"); - }), - TestCase("succeeds for non-numeric non-string ID", () => - { - var it = Parameters.Id(new Uri("https://example.com")); - Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@string("https://example.com/"), - "Non-numeric, non-string parameter value incorrect"); - }) - ]), - TestCase("Json succeeds", () => - { - var it = Parameters.Json("@test", new { Something = "good" }); - Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); - }), - TestList("AddFields", - [ - TestCase("succeeds when a parameter is added", () => - { - var paramList = Parameters.AddFields([Field.EQ("it", "242")], []).ToList(); - Expect.hasLength(paramList, 1, "There should have been a parameter added"); - var (name, value) = paramList[0]; - Expect.equal(name, "@field0", "Field parameter name not correct"); - Expect.equal(value, Sql.@string("242"), "Field parameter value not correct"); - }), - TestCase("succeeds when multiple independent parameters are added", () => - { - var paramList = Parameters.AddFields([Field.EQ("me", "you"), Field.GT("us", "them")], - [Parameters.Id(14)]).ToList(); - Expect.hasLength(paramList, 3, "There should have been 2 parameters added"); - var (name, value) = paramList[0]; - Expect.equal(name, "@id", "First field parameter name not correct"); - Expect.equal(value, Sql.@int(14), "First field parameter value not correct"); - (name, value) = paramList[1]; - Expect.equal(name, "@field0", "Second field parameter name not correct"); - Expect.equal(value, Sql.@string("you"), "Second field parameter value not correct"); - (name, value) = paramList[2]; - Expect.equal(name, "@field1", "Third parameter name not correct"); - Expect.equal(value, Sql.@string("them"), "Third parameter value not correct"); - }), - TestCase("succeeds when a parameter is not added", () => - { - var paramList = Parameters.AddFields([Field.EX("tacos")], []).ToList(); - Expect.isEmpty(paramList, "There should not have been any parameters added"); - }), - TestCase("succeeds when two parameters are added for one field", () => - { - var paramList = - Parameters.AddFields([Field.BT("that", "eh", "zed").WithParameterName("@test")], []).ToList(); - Expect.hasLength(paramList, 2, "There should have been 2 parameters added"); - var (name, value) = paramList[0]; - Expect.equal(name, "@testmin", "Minimum field name not correct"); - Expect.equal(value, Sql.@string("eh"), "Minimum field value not correct"); - (name, value) = paramList[1]; - Expect.equal(name, "@testmax", "Maximum field name not correct"); - Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); - }) - ]), - TestList("FieldNames", - [ - TestCase("succeeds for one name", () => - { - var (name, value) = Parameters.FieldNames(["bob"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsString) - { - Expect.isTrue(false, "The parameter was not a String type"); - } - }), - TestCase("succeeds for multiple names", () => - { - var (name, value) = Parameters.FieldNames(["bob", "tom", "mike"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsStringArray) - { - Expect.isTrue(false, "The parameter was not a StringArray type"); - } - }) - ]), -#pragma warning disable CS0618 - TestList("FieldName", - [ - TestCase("succeeds for one name", () => - { - var (name, value) = Parameters.FieldName(["bob"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsString) - { - Expect.isTrue(false, "The parameter was not a String type"); - } - }), - TestCase("succeeds for multiple names", () => - { - var (name, value) = Parameters.FieldName(["bob", "tom", "mike"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsStringArray) - { - Expect.isTrue(false, "The parameter was not a StringArray type"); - } - }) - ]) -#pragma warning restore CS0618 - ]), - TestList("Query", - [ - TestList("WhereByFields", - [ - TestCase("succeeds for a single field when a logical operator is passed", () => - { - Expect.equal( - Postgres.Query.WhereByFields(FieldMatch.Any, - [Field.GT("theField", "0").WithParameterName("@test")]), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds for a single field when an existence operator is passed", () => - { - Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), - "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(FieldMatch.All, - [Field.BT("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", () => - { - Expect.equal( - Postgres.Query.WhereByFields(FieldMatch.Any, - [Field.BT("field0", "a", "b").WithParameterName("@alpha")]), - "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); - }), - TestCase("succeeds for all multiple fields with logical operators", () => - { - Expect.equal( - Postgres.Query.WhereByFields(FieldMatch.All, - [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), - "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(FieldMatch.Any, - [Field.NEX("thatField"), Field.GE("thisField", 18)]), - "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0", - "WHERE clause not correct"); - }), - TestCase("succeeds for all multiple fields with between operators", () => - { - Expect.equal( - Postgres.Query.WhereByFields(FieldMatch.All, - [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), - "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", - "WHERE clause not correct"); - }) - ]), - TestList("WhereById", - [ - TestCase("succeeds for numeric ID", () => - { - Expect.equal(Postgres.Query.WhereById(18), "(data->>'Id')::numeric = @id", - "WHERE clause not correct"); - }), - TestCase("succeeds for string ID", () => - { - Expect.equal(Postgres.Query.WhereById("18"), "data->>'Id' = @id", "WHERE clause not correct"); - }), - TestCase("succeeds for non-numeric non-string ID", () => - { - Expect.equal(Postgres.Query.WhereById(new Uri("https://example.com")), "data->>'Id' = @id", - "WHERE clause not correct"); - }), - ]), - TestList("Definition", - [ - TestCase("EnsureTable succeeds", () => - { - Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), - $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)", - "CREATE TABLE statement not constructed correctly"); - }), - TestCase("EnsureDocumentIndex succeeds for full index", () => - { - Expect.equal(Postgres.Query.Definition.EnsureDocumentIndex("schema.tbl", DocumentIndex.Full), - "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)", - "CREATE INDEX statement not constructed correctly"); - }), - TestCase("EnsureDocumentIndex succeeds for JSONB Path Ops index", () => - { - Expect.equal( - Postgres.Query.Definition.EnsureDocumentIndex(PostgresDb.TableName, DocumentIndex.Optimized), - string.Format( - "CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)", - PostgresDb.TableName), - "CREATE INDEX statement not constructed correctly"); - }) - ]), - TestCase("WhereDataContains succeeds", () => - { - Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", - "WHERE clause not correct"); - }), - TestCase("WhereJsonPathMatches succeeds", () => - { - Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", - "WHERE clause not correct"); - }), - TestCase("Patch succeeds", () => - { - Expect.equal(Postgres.Query.Patch(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data", "Patch query not correct"); - }), - TestCase("RemoveFields succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name", "Field removal query not correct"); - }), - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", - "By-ID query not correct"); - }), - TestCase("ByFields succeeds", () => - { - Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), - "unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct"); - }), - TestCase ("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.ByContains("exam"), "exam WHERE data @> @criteria", - "By-Contains query not correct"); - }), - TestCase("ByPathMach succeeds", () => - { - Expect.equal(Postgres.Query.ByPathMatch("verify"), "verify WHERE data @? @path::jsonpath", - "By-JSON Path query not correct"); - }) - ]) + ParametersTests, + QueryTests ]); private static readonly List TestDocuments = @@ -357,984 +360,1171 @@ public static class PostgresCSharpTests } /// - /// Integration tests for the PostgreSQL library + /// Integration tests for the Configuration module of the PostgreSQL library /// - private static readonly Test Integration = TestList("Integration", + private static readonly Test ConfigurationTests = TestList("Configuration", [ - TestList("Configuration", - [ - TestCase("UseDataSource disposes existing source", () => - { - using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - var source = PostgresDb.MkDataSource(db1.ConnectionString); - Postgres.Configuration.UseDataSource(source); + TestCase("UseDataSource disposes existing source", () => + { + using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + var source = PostgresDb.MkDataSource(db1.ConnectionString); + Postgres.Configuration.UseDataSource(source); - using var db2 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - Postgres.Configuration.UseDataSource(PostgresDb.MkDataSource(db2.ConnectionString)); + using var db2 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + Postgres.Configuration.UseDataSource(PostgresDb.MkDataSource(db2.ConnectionString)); + try + { + _ = source.OpenConnection(); + Expect.isTrue(false, "Data source should have been disposed"); + } + catch (Exception) + { + // This is what should have happened + } + }), + TestCase("DataSource returns configured data source", () => + { + using var db = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + var source = PostgresDb.MkDataSource(db.ConnectionString); + Postgres.Configuration.UseDataSource(source); + + Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), + "Data source should have been the same"); + }) + ]); + + /// + /// Integration tests for the Custom module of the PostgreSQL library + /// + private static readonly Test CustomTests = TestList("Custom", + [ + TestList("List", + [ + TestCase("succeeds when data is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List(Query.Find(PostgresDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); + }), + TestCase("succeeds when data is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List( + $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))], Results.FromData); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("Single", + [ + TestCase("succeeds when a row is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", + [Tuple.Create("@id", Sql.@string("one"))], Results.FromData); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when a row is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", + [Tuple.Create("@id", Sql.@string("eighty"))], Results.FromData); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("NonQuery", + [ + TestCase("succeeds when operating on data", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None); + + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 0, "There should be no documents remaining in the table"); + }), + TestCase("succeeds when no data matches where clause", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))]); + + 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(); + + 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"); + }) + ]); + + /// + /// Integration tests for the Definition module of the PostgreSQL library + /// + private static readonly Test DefinitionTests = TestList("Definition", + [ + TestCase("EnsureTable succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + + 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(); + 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 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(); + 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 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(); + 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); + }) + ]); + + /// + /// Integration tests for the Document module of the PostgreSQL library + /// + private static readonly Test DocumentTests = TestList("Document", + [ + TestList("Insert", + [ + TestCase("succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await Count.All(PostgresDb.TableName); + Expect.equal(after, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = PostgresDb.BuildDb(); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); try { - _ = source.OpenConnection(); - Expect.isTrue(false, "Data source should have been disposed"); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); } catch (Exception) { // This is what should have happened } - }), - TestCase("DataSource returns configured data source", () => - { - using var db = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - var source = PostgresDb.MkDataSource(db.ConnectionString); - Postgres.Configuration.UseDataSource(source); - - Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), - "Data source should have been the same"); }) ]), - TestList("Custom", + TestList("Save", [ - TestList("List", - [ - TestCase("succeeds when data is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Custom.List(Query.Find(PostgresDb.TableName), Parameters.None, - Results.FromData); - Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); - }), - TestCase("succeeds when data is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Custom.List( - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }, - Results.FromData); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("Single", - [ - TestCase("succeeds when a row is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when a row is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("NonQuery", - [ - TestCase("succeeds when operating on data", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None); - - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 0, "There should be no documents remaining in the table"); - }), - TestCase("succeeds when no data matches where clause", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }); - - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); - }) - ]), - TestCase("Scalar succeeds", async () => + TestCase("succeeds when a document is inserted", async () => { await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); - 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"); + await Document.Save(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await Count.All(PostgresDb.TableName); + Expect.equal(after, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await Document.Insert(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await Find.ById(PostgresDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before.Id, "test", "The document is not correct"); + + before.Sub = new() { Foo = "c", Bar = "d" }; + await Document.Save(PostgresDb.TableName, before); + var after = await Find.ById(PostgresDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "test", "The document is not correct"); + Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); }) - ]), - TestList("Definition", - [ - TestCase("EnsureTable succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - - 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(); - 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 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(); - 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 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(); - 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", - [ - TestList("Insert", - [ - TestCase("succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should be no documents in the table"); - - await Document.Insert(PostgresDb.TableName, - new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); - var after = await Count.All(PostgresDb.TableName); - Expect.equal(after, 1, "There should have been one document inserted"); - }), - TestCase("fails for duplicate key", async () => - { - await using var db = PostgresDb.BuildDb(); - await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); - try - { - await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); - Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); - } - catch (Exception) - { - // This is what should have happened - } - }) - ]), - TestList("Save", - [ - TestCase("succeeds when a document is inserted", async () => - { - await using var db = PostgresDb.BuildDb(); - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should be no documents in the table"); - - await Document.Save(PostgresDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - var after = await Count.All(PostgresDb.TableName); - Expect.equal(after, 1, "There should have been one document inserted"); - }), - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await Document.Insert(PostgresDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - - var before = await Find.ById(PostgresDb.TableName, "test"); - Expect.isNotNull(before, "There should have been a document returned"); - Expect.equal(before.Id, "test", "The document is not correct"); - - before.Sub = new() { Foo = "c", Bar = "d" }; - await Document.Save(PostgresDb.TableName, before); - var after = await Find.ById(PostgresDb.TableName, "test"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "test", "The document is not correct"); - Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); - }) - ]) - ]), - TestList("Count", - [ - TestCase("All succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.All(PostgresDb.TableName); - Expect.equal(theCount, 5, "There should have been 5 matching documents"); - }), - TestList("ByFields", - [ - TestCase("succeeds for numeric range", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.BT("NumValue", 10, 20)]); - Expect.equal(theCount, 3, "There should have been 3 matching documents"); - }), - TestCase("succeeds for non-numeric range", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.All, - [Field.BT("Value", "aardvark", "apple")]); - Expect.equal(theCount, 1, "There should have been 1 matching document"); - }) - ]), - TestCase("ByContains succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByContains(PostgresDb.TableName, new { Value = "purple" }); - Expect.equal(theCount, 2, "There should have been 2 matching documents"); - }), - TestCase("ByJsonPath succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); - Expect.equal(theCount, 3, "There should have been 3 matching documents"); - }) - ]), - TestList("Exists", - [ - TestList("ById", - [ - TestCase("succeeds when a document exists", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(PostgresDb.TableName, "three"); - Expect.isTrue(exists, "There should have been an existing document"); - }), - TestCase("succeeds when a document does not exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(PostgresDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NEX("Sub")]); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when documents do not exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("NumValue", "six")]); - Expect.isFalse(exists, "There should not have been existing documents"); - }) - ]), - TestList("ByContains", - [ - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByContains(PostgresDb.TableName, new { NumValue = 10 }); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - ]), - TestList("ByJsonPath", - [ - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - ]) - ]), - TestList("Find", - [ - TestList("All", - [ - TestCase("succeeds when there is data", async () => - { - await using var db = PostgresDb.BuildDb(); - - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "one", Bar = "two" }); - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "three", Bar = "four" }); - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "five", Bar = "six" }); - - var results = await Find.All(PostgresDb.TableName); - Expect.equal(results.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when there is no data", async () => - { - await using var db = PostgresDb.BuildDb(); - var results = await Find.All(PostgresDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) - ]), - TestList("ById", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(PostgresDb.TableName, "three hundred eighty-seven"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "another")]); - Expect.equal(docs.Count, 1, "There should have been one document returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "mauve")]); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("ByContains", - [ - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByContains(PostgresDb.TableName, - new { Sub = new { Foo = "green" } }); - Expect.equal(docs.Count, 2, "There should have been two documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByContains(PostgresDb.TableName, new { Value = "mauve" }); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("ByJsonPath", - [ - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 15)"); - Expect.equal(docs.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("FirstByFields", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "another")]); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "purple")]); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "absent")]); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("FirstByContains", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "another" }); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, - new { Sub = new { Foo = "green" } }); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "absent" }); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("FirstByJsonPath", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, - "$.Value ? (@ == \"FIRST!\")"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, - "$.Sub.Foo ? (@ == \"green\")"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]) - ]), - TestList("Update", - [ - TestList("ById", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Update.ById(PostgresDb.TableName, "one", - new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); - Expect.equal(after.Value, "", "The updated document is not correct (Value)"); - Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)"); - Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); - Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)"); - Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ById(PostgresDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) - ]), - TestList("ByFunc", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); - Expect.equal(after.Value, "le un", "The updated document is not correct (Value)"); - Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)"); - Expect.isNull(after.Sub, "The updated document should not have had a sub-document"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - ]) - ]), - TestList("Patch", - [ - TestList("ById", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ById(PostgresDb.TableName, "one", new { NumValue = 44 }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.NumValue, 44, "The updated document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], - new { NumValue = 77 }); - var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.EQ("NumValue", "77")]); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], - new { Foo = "green" }); - }) - ]), - TestList("ByContains", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 }); - var after = await Count.ByContains(PostgresDb.TableName, new { NumValue = 77 }); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); - }) - ]), - TestList("ByJsonPath", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 }); - var after = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)"); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); - }) - ]) - ]), - TestList("RemoveFields", - [ - TestList("ById", - [ - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], - new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], - new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, - [Field.NE("Abracadabra", "apple")], new[] { "Value" }); - }) - ]), - TestList("ByContains", - [ - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, - new[] { "Value" }); - }) - ]), - TestList("ByJsonPath", - [ - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", - new[] { "Value" }); - }) - ]) - ]), - TestList("Delete", - [ - TestList("ById", - [ - TestCase("succeeds when a document is deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(PostgresDb.TableName, "four"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 4, "There should have been 4 documents remaining"); - }), - TestCase("succeeds when a document is not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(PostgresDb.TableName, "thirty"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - ]), - TestList("ByContains", - [ - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByContains(PostgresDb.TableName, new { Value = "purple" }); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByContains(PostgresDb.TableName, new { Value = "crimson" }); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - ]), - TestList("ByJsonPath", - [ - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - ]) ]) ]); - + + /// + /// Integration tests for the Count module of the PostgreSQL library + /// + private static readonly Test CountTests = TestList("Count", + [ + TestCase("All succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.All(PostgresDb.TableName); + Expect.equal(theCount, 5, "There should have been 5 matching documents"); + }), + TestList("ByFields", + [ + TestCase("succeeds for numeric range", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.BT("NumValue", 10, 20)]); + Expect.equal(theCount, 3, "There should have been 3 matching documents"); + }), + TestCase("succeeds for non-numeric range", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.All, + [Field.BT("Value", "aardvark", "apple")]); + Expect.equal(theCount, 1, "There should have been 1 matching document"); + }) + ]), + TestCase("ByContains succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByContains(PostgresDb.TableName, new { Value = "purple" }); + Expect.equal(theCount, 2, "There should have been 2 matching documents"); + }), + TestCase("ByJsonPath succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); + Expect.equal(theCount, 3, "There should have been 3 matching documents"); + }) + ]); + + /// + /// Integration tests for the Exists module of the PostgreSQL library + /// + private static readonly Test ExistsTests = TestList("Exists", + [ + TestList("ById", + [ + TestCase("succeeds when a document exists", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(PostgresDb.TableName, "three"); + Expect.isTrue(exists, "There should have been an existing document"); + }), + TestCase("succeeds when a document does not exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(PostgresDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NEX("Sub")]); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when documents do not exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]); + Expect.isFalse(exists, "There should not have been existing documents"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByContains(PostgresDb.TableName, new { NumValue = 10 }); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when no matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when no matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + ]) + ]); + + /// + /// Integration tests for the Find module of the PostgreSQL library + /// + private static readonly Test FindTests = TestList("Find", + [ + TestList("All", + [ + TestCase("succeeds when there is data", async () => + { + await using var db = PostgresDb.BuildDb(); + + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "one", Bar = "two" }); + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "three", Bar = "four" }); + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "five", Bar = "six" }); + + var results = await Find.All(PostgresDb.TableName); + Expect.hasLength(results, 3, "There should have been 3 documents returned"); + }), + TestCase("succeeds when there is no data", async () => + { + await using var db = PostgresDb.BuildDb(); + var results = await Find.All(PostgresDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + ]), + TestList("AllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = + await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = + await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = await Find.AllOrdered(PostgresDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("ById", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(PostgresDb.TableName, "three hundred eighty-seven"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.hasLength(docs, 1, "There should have been one document returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContains(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContains(PostgresDb.TableName, new { Value = "mauve" }); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByContainsOrdered", + [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|four", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar DESC")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|two", + "Documents not ordered correctly"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 15)"); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByJsonPathOrdered", + [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "one|three|two", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue DESC")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|three|one", + "Documents not ordered correctly"); + }) + ]), + TestList("FirstByFields", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("five", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("FirstByContains", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "another" }); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "absent" }); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByJsonPath", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, + "$.Value ? (@ == \"FIRST!\")"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]) + ]); + + /// + /// Integration tests for the Update module of the PostgreSQL library + /// + private static readonly Test UpdateTests = TestList("Update", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.ById(PostgresDb.TableName, "one", + new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); + Expect.equal(after.Value, "", "The updated document is not correct (Value)"); + Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)"); + Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); + Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)"); + Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ById(PostgresDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + ]), + TestList("ByFunc", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); + Expect.equal(after.Value, "le un", "The updated document is not correct (Value)"); + Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)"); + Expect.isNull(after.Sub, "The updated document should not have had a sub-document"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + ]) + ]); + + /// + /// Integration tests for the Patch module of the PostgreSQL library + /// + private static readonly Test PatchTests = TestList("Patch", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ById(PostgresDb.TableName, "one", new { NumValue = 44 }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.NumValue, 44, "The updated document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "77")]); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 }); + var after = await Count.ByContains(PostgresDb.TableName, new { NumValue = 77 }); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 }); + var after = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)"); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + }) + ]) + ]); + + /// + /// Integration tests for the RemoveFields module of the PostgreSQL library + /// + private static readonly Test RemoveFieldsTests = TestList("RemoveFields", + [ + TestList("ById", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(PostgresDb.TableName, "two", ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(PostgresDb.TableName, "two", ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ById(PostgresDb.TableName, "two", ["AFieldThatIsNotThere"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ById(PostgresDb.TableName, "two", ["Value"]); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, ["Value"]); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", ["Value"]); + }) + ]) + ]); + + /// + /// Integration tests for the Delete module of the PostgreSQL library + /// + private static readonly Test DeleteTests = TestList("Delete", + [ + TestList("ById", + [ + TestCase("succeeds when a document is deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(PostgresDb.TableName, "four"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 4, "There should have been 4 documents remaining"); + }), + TestCase("succeeds when a document is not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(PostgresDb.TableName, "thirty"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByContains(PostgresDb.TableName, new { Value = "purple" }); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByContains(PostgresDb.TableName, new { Value = "crimson" }); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)"); + 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#", [Unit, TestSequenced(Integration)]); + public static readonly Test All = TestList("Postgres.C#", + [ + TestList("Unit", + [ + ParametersTests, + QueryTests + ]), + TestSequenced(TestList("Integration", + [ + ConfigurationTests, + CustomTests, + DefinitionTests, + DocumentTests, + CountTests, + ExistsTests, + FindTests, + UpdateTests, + PatchTests, + RemoveFieldsTests, + DeleteTests + ])) + ]); } diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 0a2e3ba..1c04aaa 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -7,1118 +7,1249 @@ open BitBadger.Documents.Tests #nowarn "0044" -/// Tests which do not hit the database -let unitTests = - testList "Unit" [ - testList "Parameters" [ - testList "idParam" [ - // NOTE: these tests also exercise all branches of the internal parameterFor function - test "succeeds for byte ID" { - Expect.equal - (idParam (sbyte 7)) ("@id", Sql.int8 (sbyte 7)) "Byte ID parameter not constructed correctly" - } - test "succeeds for unsigned byte ID" { - Expect.equal - (idParam (byte 7)) - ("@id", Sql.int8 (int8 (byte 7))) - "Unsigned byte ID parameter not constructed correctly" - } - test "succeeds for short ID" { - Expect.equal - (idParam (int16 44)) - ("@id", Sql.int16 (int16 44)) - "Short ID parameter not constructed correctly" - } - test "succeeds for unsigned short ID" { - Expect.equal - (idParam (uint16 64)) - ("@id", Sql.int16 (int16 64)) - "Unsigned short ID parameter not constructed correctly" - } - test "succeeds for integer ID" { - Expect.equal (idParam 88) ("@id", Sql.int 88) "Int ID parameter not constructed correctly" - } - test "succeeds for unsigned integer ID" { - Expect.equal - (idParam (uint 889)) ("@id", Sql.int 889) "Unsigned int ID parameter not constructed correctly" - } - test "succeeds for long ID" { - Expect.equal - (idParam (int64 123)) - ("@id", Sql.int64 (int64 123)) - "Long ID parameter not constructed correctly" - } - test "succeeds for unsigned long ID" { - Expect.equal - (idParam (uint64 6464)) - ("@id", Sql.int64 (int64 6464)) - "Unsigned long ID parameter not constructed correctly" - } - test "succeeds for decimal ID" { - Expect.equal - (idParam (decimal 4.56)) - ("@id", Sql.decimal (decimal 4.56)) - "Decimal ID parameter not constructed correctly" - } - test "succeeds for single ID" { - Expect.equal - (idParam (single 5.67)) - ("@id", Sql.double (double (single 5.67))) - "Single ID parameter not constructed correctly" - } - test "succeeds for double ID" { - Expect.equal - (idParam (double 6.78)) - ("@id", Sql.double (double 6.78)) - "Double ID parameter not constructed correctly" - } - test "succeeds for string ID" { - Expect.equal (idParam "99") ("@id", Sql.string "99") "String ID parameter not constructed correctly" - } - test "succeeds for non-numeric non-string ID" { - let target = { new obj() with override _.ToString() = "ToString was called" } - Expect.equal - (idParam target) - ("@id", Sql.string "ToString was called") - "Non-numeric, non-string parameter not constructed correctly" - } - ] - test "jsonParam succeeds" { - Expect.equal - (jsonParam "@test" {| Something = "good" |}) - ("@test", Sql.jsonb """{"Something":"good"}""") - "JSON parameter not constructed correctly" - } - testList "addFieldParams" [ - test "succeeds when a parameter is added" { - let paramList = addFieldParams [ Field.EQ "it" "242" ] [] - Expect.hasLength paramList 1 "There should have been a parameter added" - let name, value = Seq.head paramList - Expect.equal name "@field0" "Field parameter name not correct" - Expect.equal value (Sql.string "242") "Parameter value not correct" - } - test "succeeds when multiple independent parameters are added" { - let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ] - Expect.hasLength paramList 3 "There should have been 2 parameters added" - let p = Array.ofSeq paramList - Expect.equal (fst p[0]) "@id" "First field parameter name not correct" - Expect.equal (snd p[0]) (Sql.int 14) "First parameter value not correct" - Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct" - Expect.equal (snd p[1]) (Sql.string "you") "Second parameter value not correct" - Expect.equal (fst p[2]) "@field1" "Third parameter name not correct" - Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct" - } - test "succeeds when a parameter is not added" { - let paramList = addFieldParams [ Field.EX "tacos" ] [] - Expect.isEmpty paramList "There should not have been any parameters added" - } - test "succeeds when two parameters are added for one field" { - let paramList = - addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] [] - Expect.hasLength paramList 2 "There should have been 2 parameters added" - let name, value = Seq.head paramList - Expect.equal name "@testmin" "Minimum field name not correct" - Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" - let name, value = paramList |> Seq.skip 1 |> Seq.head - Expect.equal name "@testmax" "Maximum field name not correct" - Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" - } - ] - testList "fieldNameParams" [ - test "succeeds for one name" { - let name, value = fieldNameParams [ "bob" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.string "bob") "The parameter value was incorrect" - } - test "succeeds for multiple names" { - let name, value = fieldNameParams [ "bob"; "tom"; "mike" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" - } - ] - testList "fieldNameParam" [ - test "succeeds for one name" { - let name, value = fieldNameParam [ "bob" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.string "bob") "The parameter value was incorrect" - } - test "succeeds for multiple names" { - let name, value = fieldNameParam [ "bob"; "tom"; "mike" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" - } - ] - test "noParams succeeds" { - Expect.isEmpty noParams "The no-params sequence should be empty" - } - ] - testList "Query" [ - testList "whereByFields" [ - test "succeeds for a single field when a logical operator is passed" { - Expect.equal - (Query.whereByFields Any [ { Field.GT "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" { - Expect.equal - (Query.whereByFields Any [ Field.NEX "thatField" ]) - "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 All [ { Field.BT "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" { - Expect.equal - (Query.whereByFields Any [ { Field.BT "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" { - Expect.equal - (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) - "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 Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) - "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0" - "WHERE clause not correct" - } - test "succeeds for all multiple fields with between operators" { - Expect.equal - (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) - "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" - "WHERE clause not correct" - } - ] - testList "whereById" [ - test "succeeds for numeric ID" { - Expect.equal (Query.whereById 18) "(data->>'Id')::numeric = @id" "WHERE clause not correct" - } - test "succeeds for string ID" { - Expect.equal (Query.whereById "18") "data->>'Id' = @id" "WHERE clause not correct" - } - test "succeeds for non-numeric non-string ID" { - Expect.equal - (Query.whereById (System.Uri "https://example.com")) - "data->>'Id' = @id" - "WHERE clause not correct" - } - ] - testList "Definition" [ - test "ensureTable succeeds" { - Expect.equal - (Query.Definition.ensureTable PostgresDb.TableName) - $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" - "CREATE TABLE statement not constructed correctly" - } - test "ensureDocumentIndex succeeds for full index" { - Expect.equal - (Query.Definition.ensureDocumentIndex "schema.tbl" Full) - "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)" - "CREATE INDEX statement not constructed correctly" - } - test "ensureDocumentIndex succeeds for JSONB Path Ops index" { - Expect.equal - (Query.Definition.ensureDocumentIndex PostgresDb.TableName Optimized) - (sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)" - PostgresDb.TableName PostgresDb.TableName) - "CREATE INDEX statement not constructed correctly" - } - ] - test "whereDataContains succeeds" { - Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" - } - test "whereJsonPathMatches succeeds" { - Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" - } - test "patch succeeds" { - Expect.equal - (Query.patch PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data" - "Patch query not correct" - } - test "removeFields succeeds" { - Expect.equal - (Query.removeFields PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data - @name" - "Field removal query not correct" - } - test "byId succeeds" { - Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.byFields "unit" Any [ Field.GT "That" 14 ]) - "unit WHERE (data->>'That')::numeric > @field0" - "By-Field query not correct" - } - test "byContains succeeds" { - Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct" - } - test "byPathMach succeeds" { - Expect.equal - (Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query not correct" - } - ] +(** UNIT TESTS **) + +/// Unit tests for the Parameters module of the PostgreSQL library +let parametersTests = testList "Parameters" [ + testList "idParam" [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + test "succeeds for byte ID" { + Expect.equal (idParam (sbyte 7)) ("@id", Sql.int8 (sbyte 7)) "Byte ID parameter not constructed correctly" + } + test "succeeds for unsigned byte ID" { + Expect.equal + (idParam (byte 7)) + ("@id", Sql.int8 (int8 (byte 7))) + "Unsigned byte ID parameter not constructed correctly" + } + test "succeeds for short ID" { + Expect.equal + (idParam (int16 44)) ("@id", Sql.int16 (int16 44)) "Short ID parameter not constructed correctly" + } + test "succeeds for unsigned short ID" { + Expect.equal + (idParam (uint16 64)) + ("@id", Sql.int16 (int16 64)) + "Unsigned short ID parameter not constructed correctly" + } + test "succeeds for integer ID" { + Expect.equal (idParam 88) ("@id", Sql.int 88) "Int ID parameter not constructed correctly" + } + test "succeeds for unsigned integer ID" { + Expect.equal (idParam (uint 889)) ("@id", Sql.int 889) "Unsigned int ID parameter not constructed correctly" + } + test "succeeds for long ID" { + Expect.equal + (idParam (int64 123)) ("@id", Sql.int64 (int64 123)) "Long ID parameter not constructed correctly" + } + test "succeeds for unsigned long ID" { + Expect.equal + (idParam (uint64 6464)) + ("@id", Sql.int64 (int64 6464)) + "Unsigned long ID parameter not constructed correctly" + } + test "succeeds for decimal ID" { + Expect.equal + (idParam (decimal 4.56)) + ("@id", Sql.decimal (decimal 4.56)) + "Decimal ID parameter not constructed correctly" + } + test "succeeds for single ID" { + Expect.equal + (idParam (single 5.67)) + ("@id", Sql.double (double (single 5.67))) + "Single ID parameter not constructed correctly" + } + test "succeeds for double ID" { + Expect.equal + (idParam (double 6.78)) + ("@id", Sql.double (double 6.78)) + "Double ID parameter not constructed correctly" + } + test "succeeds for string ID" { + Expect.equal (idParam "99") ("@id", Sql.string "99") "String ID parameter not constructed correctly" + } + test "succeeds for non-numeric non-string ID" { + let target = { new obj() with override _.ToString() = "ToString was called" } + Expect.equal + (idParam target) + ("@id", Sql.string "ToString was called") + "Non-numeric, non-string parameter not constructed correctly" + } ] + test "jsonParam succeeds" { + Expect.equal + (jsonParam "@test" {| Something = "good" |}) + ("@test", Sql.jsonb """{"Something":"good"}""") + "JSON parameter not constructed correctly" + } + testList "addFieldParams" [ + test "succeeds when a parameter is added" { + let paramList = addFieldParams [ Field.EQ "it" "242" ] [] + Expect.hasLength paramList 1 "There should have been a parameter added" + let name, value = Seq.head paramList + Expect.equal name "@field0" "Field parameter name not correct" + Expect.equal value (Sql.string "242") "Parameter value not correct" + } + test "succeeds when multiple independent parameters are added" { + let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ] + Expect.hasLength paramList 3 "There should have been 2 parameters added" + let p = Array.ofSeq paramList + Expect.equal (fst p[0]) "@id" "First field parameter name not correct" + Expect.equal (snd p[0]) (Sql.int 14) "First parameter value not correct" + Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct" + Expect.equal (snd p[1]) (Sql.string "you") "Second parameter value not correct" + Expect.equal (fst p[2]) "@field1" "Third parameter name not correct" + Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct" + } + test "succeeds when a parameter is not added" { + let paramList = addFieldParams [ Field.EX "tacos" ] [] + Expect.isEmpty paramList "There should not have been any parameters added" + } + test "succeeds when two parameters are added for one field" { + let paramList = addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] [] + Expect.hasLength paramList 2 "There should have been 2 parameters added" + let name, value = Seq.head paramList + Expect.equal name "@testmin" "Minimum field name not correct" + Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" + let name, value = paramList |> Seq.skip 1 |> Seq.head + Expect.equal name "@testmax" "Maximum field name not correct" + Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" + } + ] + testList "fieldNameParams" [ + test "succeeds for one name" { + let name, value = fieldNameParams [ "bob" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.string "bob") "The parameter value was incorrect" + } + test "succeeds for multiple names" { + let name, value = fieldNameParams [ "bob"; "tom"; "mike" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" + } + ] + testList "fieldNameParam" [ + test "succeeds for one name" { + let name, value = fieldNameParam [ "bob" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.string "bob") "The parameter value was incorrect" + } + test "succeeds for multiple names" { + let name, value = fieldNameParam [ "bob"; "tom"; "mike" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" + } + ] + test "noParams succeeds" { + Expect.isEmpty noParams "The no-params sequence should be empty" + } +] + +/// 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" { + Expect.equal + (Query.whereByFields Any [ { Field.GT "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" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField" ]) + "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 All [ { Field.BT "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" { + Expect.equal + (Query.whereByFields Any [ { Field.BT "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" { + Expect.equal + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) + "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 Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with between operators" { + Expect.equal + (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" + } + ] + testList "whereById" [ + test "succeeds for numeric ID" { + Expect.equal (Query.whereById 18) "(data->>'Id')::numeric = @id" "WHERE clause not correct" + } + test "succeeds for string ID" { + Expect.equal (Query.whereById "18") "data->>'Id' = @id" "WHERE clause not correct" + } + test "succeeds for non-numeric non-string ID" { + Expect.equal + (Query.whereById (System.Uri "https://example.com")) "data->>'Id' = @id" "WHERE clause not correct" + } + ] + testList "Definition" [ + test "ensureTable succeeds" { + Expect.equal + (Query.Definition.ensureTable PostgresDb.TableName) + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" + "CREATE TABLE statement not constructed correctly" + } + test "ensureDocumentIndex succeeds for full index" { + Expect.equal + (Query.Definition.ensureDocumentIndex "schema.tbl" Full) + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)" + "CREATE INDEX statement not constructed correctly" + } + test "ensureDocumentIndex succeeds for JSONB Path Ops index" { + Expect.equal + (Query.Definition.ensureDocumentIndex PostgresDb.TableName Optimized) + (sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)" + PostgresDb.TableName PostgresDb.TableName) + "CREATE INDEX statement not constructed correctly" + } + ] + test "whereDataContains succeeds" { + Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" + } + test "whereJsonPathMatches succeeds" { + Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" + } + test "patch succeeds" { + Expect.equal + (Query.patch PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE (data->>'That')::numeric > @field0" + "By-Field query not correct" + } + test "byContains succeeds" { + Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct" + } + test "byPathMach succeeds" { + Expect.equal + (Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query not correct" + } +] + +(** INTEGRATION TESTS **) open ThrowawayDb.Postgres open Types -let isTrue<'T> (_ : 'T) = true +/// Documents to use for integration tests +let documents = [ + { 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 integrationTests = - let documents = [ - { 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 loadDocs () = backgroundTask { - for doc in documents do do! insert PostgresDb.TableName doc +/// Load the test documents into the database +let loadDocs () = backgroundTask { + for doc in documents do do! insert PostgresDb.TableName doc +} + +/// Integration tests for the Configuration module of the PostgreSQL library +let configurationTests = testList "Configuration" [ + test "useDataSource disposes existing source" { + use db1 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + let source = PostgresDb.MkDataSource db1.ConnectionString + Configuration.useDataSource source + + use db2 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + Configuration.useDataSource (PostgresDb.MkDataSource db2.ConnectionString) + Expect.throws (fun () -> source.OpenConnection() |> ignore) "Data source should have been disposed" } - testList "Integration" [ - testList "Configuration" [ - test "useDataSource disposes existing source" { - use db1 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - let source = PostgresDb.MkDataSource db1.ConnectionString - Configuration.useDataSource source - - use db2 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - Configuration.useDataSource (PostgresDb.MkDataSource db2.ConnectionString) - Expect.throws (fun () -> source.OpenConnection() |> ignore) "Data source should have been disposed" - } - test "dataSource returns configured data source" { - use db = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - let source = PostgresDb.MkDataSource db.ConnectionString - Configuration.useDataSource source - - Expect.isTrue (obj.ReferenceEquals(source, Configuration.dataSource ())) - "Data source should have been the same" - } - ] - testList "Custom" [ - testList "list" [ - testTask "succeeds when data is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData - Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" - } - testTask "succeeds when data is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = - Custom.list $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] fromData - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "single" [ - testTask "succeeds when a row is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" - [ "@id", Sql.string "one"] fromData - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when a row is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" - [ "@id", Sql.string "eighty" ] fromData - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "nonQuery" [ - testTask "succeeds when operating on data" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" [] - - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 0 "There should be no documents remaining in the table" - } - testTask "succeeds when no data matches where clause" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] - - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should be 5 documents remaining in the table" - } - ] - testTask "scalar succeeds" { - use db = PostgresDb.BuildDb() - let! nbr = Custom.scalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value") - Expect.equal nbr 5 "The query should have returned the number 5" - } - ] - testList "Definition" [ - testTask "ensureTable succeeds" { - use db = PostgresDb.BuildDb() - let tableExists () = - Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists - let keyExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists - - let! exists = tableExists () - let! alsoExists = keyExists () - Expect.isFalse exists "The table should not exist already" - Expect.isFalse alsoExists "The key index should not exist already" - - do! Definition.ensureTable "ensured" - let! exists' = tableExists () - let! alsoExists' = keyExists () - Expect.isTrue exists' "The table should now exist" - Expect.isTrue alsoExists' "The key index should now exist" - } - testTask "ensureDocumentIndex succeeds" { - use db = PostgresDb.BuildDb() - let indexExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it" - [] - toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureDocumentIndex "ensured" Optimized - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - testTask "ensureFieldIndex succeeds" { - use db = PostgresDb.BuildDb() - let indexExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - ] - testList "insert" [ - testTask "succeeds" { - use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } - do! insert PostgresDb.TableName testDoc - let! after = Find.all PostgresDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "fails for duplicate key" { - use db = PostgresDb.BuildDb() - do! insert PostgresDb.TableName { emptyDoc with Id = "test" } - Expect.throws - (fun () -> - insert PostgresDb.TableName {emptyDoc with Id = "test" } - |> Async.AwaitTask - |> Async.RunSynchronously) - "An exception should have been raised for duplicate document ID insert" - } - ] - testList "save" [ - testTask "succeeds when a document is inserted" { - use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! save PostgresDb.TableName testDoc - let! after = Find.all PostgresDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! insert PostgresDb.TableName testDoc - - let! before = Find.byId PostgresDb.TableName "test" - Expect.isSome before "There should have been a document returned" - Expect.equal before.Value testDoc "The document is not correct" - - let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } - do! save PostgresDb.TableName upd8Doc - let! after = Find.byId PostgresDb.TableName "test" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value upd8Doc "The updated document is not correct" - } - ] - testList "Count" [ - testTask "all succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.all PostgresDb.TableName - Expect.equal theCount 5 "There should have been 5 matching documents" - } - testList "byFields" [ - testTask "succeeds when items are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = - Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ] - Expect.equal theCount 3 "There should have been 3 matching documents" - } - testTask "succeeds when items are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ] - Expect.equal theCount 0 "There should have been no matching documents" - } - ] - testTask "byContains succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byContains PostgresDb.TableName {| Value = "purple" |} - Expect.equal theCount 2 "There should have been 2 matching documents" - } - testTask "byJsonPath succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)" - Expect.equal theCount 3 "There should have been 3 matching documents" - } - ] - testList "Exists" [ - testList "byId" [ - testTask "succeeds when a document exists" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId PostgresDb.TableName "three" - Expect.isTrue exists "There should have been an existing document" - } - testTask "succeeds when a document does not exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId PostgresDb.TableName "seven" - Expect.isFalse exists "There should not have been an existing document" - } - ] - testList "byFields" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ] - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when documents do not exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = - Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ] - Expect.isFalse exists "There should not have been existing documents" - } - ] - testList "byContains" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byContains PostgresDb.TableName {| NumValue = 10 |} - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byContains PostgresDb.TableName {| Nothing = "none" |} - Expect.isFalse exists "There should not have been any existing documents" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)" - Expect.isFalse exists "There should not have been any existing documents" - } - ] - ] - testList "Find" [ - testList "all" [ - testTask "succeeds when there is data" { - use db = PostgresDb.BuildDb() - - do! insert PostgresDb.TableName { Foo = "one"; Bar = "two" } - do! insert PostgresDb.TableName { Foo = "three"; Bar = "four" } - do! insert PostgresDb.TableName { Foo = "five"; Bar = "six" } - - let! results = Find.all PostgresDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" - } - testTask "succeeds when there is no data" { - use db = PostgresDb.BuildDb() - let! results = Find.all PostgresDb.TableName - Expect.equal results [] "There should have been no documents returned" - } - ] - ftestList "allOrdered" [ - testTask "succeeds when ordering numerically" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] - Expect.hasLength results 5 "There should have been 5 documents returned" - Expect.equal - (results |> List.map _.Id |> String.concat "|") - "one|three|two|four|five" - "The documents were not ordered correctly" - } - testTask "succeeds when ordering alphabetically" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] - Expect.hasLength results 5 "There should have been 5 documents returned" - Expect.equal - (results |> List.map _.Id |> String.concat "|") - "two|three|one|four|five" - "The documents were not ordered correctly" - } - ] - testList "byId" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId PostgresDb.TableName "two" - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "byFields" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = - Find.byFields - PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] - Expect.equal (List.length docs) 1 "There should have been one document returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = - Find.byFields - PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ] - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "byContains" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} - Expect.equal (List.length docs) 2 "There should have been two documents returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byContains PostgresDb.TableName {| Value = "mauve" |} - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 15)" - Expect.equal (List.length docs) 3 "There should have been 3 documents returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "firstByFields" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "firstByContains" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Value = "another" |} - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "firstByJsonPath" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Value ? (@ == "FIRST!")""" - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" - Expect.isNone doc "There should not have been a document returned" - } - ] - ] - testList "Update" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! Update.byId PostgresDb.TableName "one" testDoc - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value testDoc "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byId - PostgresDb.TableName - "test" - { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } - } - ] - testList "byFunc" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Update.byFunc PostgresDb.TableName (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal - after.Value - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byFunc - PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - } - ] - ] - testList "Patch" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byId PostgresDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value.NumValue 44 "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} - } - ] - testList "byFields" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} - let! after = Count.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" 77 ] - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} - } - ] - testList "byContains" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} - let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |} - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} - } - ] - testList "byJsonPath" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} - let! after = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)" - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} - } - ] - ] - testList "RemoveFields" [ - testList "byId" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byId PostgresDb.TableName "two" [ "AFieldThatIsNotThere" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byId PostgresDb.TableName "two" [ "Value" ] - } - ] - testList "byFields" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] - } - ] - testList "byContains" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byContains PostgresDb.TableName {| Abracadabra = "apple" |} [ "Value" ] - } - ] - testList "byJsonPath" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] - let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byJsonPath PostgresDb.TableName "$.Abracadabra ? (@ == \"apple\")" [ "Value" ] - } - ] - ] - testList "Delete" [ - testList "byId" [ - testTask "succeeds when a document is deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byId PostgresDb.TableName "four" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 4 "There should have been 4 documents remaining" - } - testTask "succeeds when a document is not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byId PostgresDb.TableName "thirty" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byFields" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byContains" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byContains PostgresDb.TableName {| Value = "purple" |} - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byContains PostgresDb.TableName {| Value = "crimson" |} - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - ] + test "dataSource returns configured data source" { + use db = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + let source = PostgresDb.MkDataSource db.ConnectionString + Configuration.useDataSource source + + Expect.isTrue (obj.ReferenceEquals(source, Configuration.dataSource ())) "Data source should have been the same" + } +] + +/// Integration tests for the Custom module of the PostgreSQL library +let customTests = testList "Custom" [ + testList "list" [ + testTask "succeeds when data is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData + Expect.hasLength docs 5 "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Custom.list + $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" + [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } ] - |> testSequenced + testList "single" [ + testTask "succeeds when a row is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + let! doc = + Custom.single + $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" + [ "@id", Sql.string "one"] + fromData + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when a row is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () -let all = testList "Postgres" [ unitTests; integrationTests ] + let! doc = + Custom.single + $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" + [ "@id", Sql.string "eighty" ] + fromData + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "nonQuery" [ + testTask "succeeds when operating on data" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" [] + + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 0 "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery + $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" + [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] + + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use db = PostgresDb.BuildDb() + let! nbr = Custom.scalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value") + Expect.equal nbr 5 "The query should have returned the number 5" + } +] + +/// Integration tests for the Definition module of the PostgreSQL library +let definitionTests = testList "Definition" [ + testTask "ensureTable succeeds" { + use db = PostgresDb.BuildDb() + let tableExists () = + Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists + let keyExists () = + Custom.scalar + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists + + let! exists = tableExists () + let! alsoExists = keyExists () + Expect.isFalse exists "The table should not exist already" + Expect.isFalse alsoExists "The key index should not exist already" + + do! Definition.ensureTable "ensured" + let! exists' = tableExists () + let! alsoExists' = keyExists () + Expect.isTrue exists' "The table should now exist" + Expect.isTrue alsoExists' "The key index should now exist" + } + testTask "ensureDocumentIndex succeeds" { + use db = PostgresDb.BuildDb() + let indexExists () = + Custom.scalar + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it" [] toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureDocumentIndex "ensured" Optimized + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } + testTask "ensureFieldIndex succeeds" { + use db = PostgresDb.BuildDb() + let indexExists () = + Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } +] + +/// Integration tests for the (auto-opened) Document module of the PostgreSQL library +let documentTests = testList "Document" [ + testList "insert" [ + testTask "succeeds" { + use db = PostgresDb.BuildDb() + let! before = Find.all PostgresDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } + do! insert PostgresDb.TableName testDoc + let! after = Find.all PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use db = PostgresDb.BuildDb() + do! insert PostgresDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + insert PostgresDb.TableName {emptyDoc with Id = "test" } + |> Async.AwaitTask + |> Async.RunSynchronously) + "An exception should have been raised for duplicate document ID insert" + } + ] + testList "save" [ + testTask "succeeds when a document is inserted" { + use db = PostgresDb.BuildDb() + let! before = Find.all PostgresDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! save PostgresDb.TableName testDoc + let! after = Find.all PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! insert PostgresDb.TableName testDoc + + let! before = Find.byId PostgresDb.TableName "test" + Expect.isSome before "There should have been a document returned" + Expect.equal before.Value testDoc "The document is not correct" + + let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } + do! save PostgresDb.TableName upd8Doc + let! after = Find.byId PostgresDb.TableName "test" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] +] + +/// Integration tests for the Count module of the PostgreSQL library +let countTests = testList "Count" [ + testTask "all succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.all PostgresDb.TableName + Expect.equal theCount 5 "There should have been 5 matching documents" + } + testList "byFields" [ + testTask "succeeds when items are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ] + Expect.equal theCount 3 "There should have been 3 matching documents" + } + testTask "succeeds when items are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ] + Expect.equal theCount 0 "There should have been no matching documents" + } + ] + testTask "byContains succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byContains PostgresDb.TableName {| Value = "purple" |} + Expect.equal theCount 2 "There should have been 2 matching documents" + } + testTask "byJsonPath succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)" + Expect.equal theCount 3 "There should have been 3 matching documents" + } +] + +/// Integration tests for the Exists module of the PostgreSQL library +let existsTests = testList "Exists" [ + testList "byId" [ + testTask "succeeds when a document exists" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId PostgresDb.TableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId PostgresDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "byFields" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ] + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when documents do not exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ] + Expect.isFalse exists "There should not have been existing documents" + } + ] + testList "byContains" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byContains PostgresDb.TableName {| NumValue = 10 |} + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byContains PostgresDb.TableName {| Nothing = "none" |} + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)" + Expect.isFalse exists "There should not have been any existing documents" + } + ] +] + +/// Integration tests for the Find module of the PostgreSQL library +let findTests = testList "Find" [ + testList "all" [ + testTask "succeeds when there is data" { + use db = PostgresDb.BuildDb() + + do! insert PostgresDb.TableName { Foo = "one"; Bar = "two" } + do! insert PostgresDb.TableName { Foo = "three"; Bar = "four" } + do! insert PostgresDb.TableName { Foo = "five"; Bar = "six" } + + let! results = Find.all PostgresDb.TableName + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" + } + testTask "succeeds when there is no data" { + use db = PostgresDb.BuildDb() + let! results = Find.all PostgresDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "allOrdered" [ + testTask "succeeds when ordering numerically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] + testList "byId" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId PostgresDb.TableName "two" + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "byFields" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] + Expect.equal (List.length docs) 1 "There should have been one document returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ] + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly" + } + ] + testList "byContains" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byContains PostgresDb.TableName {| Value = "mauve" |} + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byContainsOrdered" [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|two" "Documents not ordered correctly" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 15)" + Expect.equal (List.length docs) 3 "There should have been 3 documents returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byJsonPathOrdered" [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "one|three|two" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue DESC" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|three|one" "Documents not ordered correctly" + } + ] + testList "firstByFields" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "five" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] + testList "firstByContains" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Value = "another" |} + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByJsonPath" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Value ? (@ == "FIRST!")""" + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" + Expect.isNone doc "There should not have been a document returned" + } + ] +] + +/// Integration tests for the Update module of the PostegreSQL library +let updateTests = testList "Update" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! Update.byId PostgresDb.TableName "one" testDoc + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value testDoc "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byId + PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "byFunc" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.byFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal + after.Value + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] +] + +/// Integration tests for the Patch module of the PostgreSQL library +let patchTests = testList "Patch" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byId PostgresDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value.NumValue 44 "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} + } + ] + testList "byFields" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" 77 ] + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} + } + ] + testList "byContains" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} + let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |} + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + } + ] + testList "byJsonPath" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} + let! after = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)" + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} + } + ] +] + +/// Integration tests for the RemoveFields module of the PostgreSQL library +let removeFieldsTests = testList "RemoveFields" [ + testList "byId" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byId PostgresDb.TableName "two" [ "AFieldThatIsNotThere" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byId PostgresDb.TableName "two" [ "Value" ] + } + ] + testList "byFields" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] + } + ] + testList "byContains" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byContains PostgresDb.TableName {| Abracadabra = "apple" |} [ "Value" ] + } + ] + testList "byJsonPath" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byJsonPath PostgresDb.TableName "$.Abracadabra ? (@ == \"apple\")" [ "Value" ] + } + ] +] + +/// Integration tests for the Delete module of the PostgreSQL library +let deleteTests = testList "Delete" [ + testList "byId" [ + testTask "succeeds when a document is deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byId PostgresDb.TableName "four" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 4 "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byId PostgresDb.TableName "thirty" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byFields" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byContains" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byContains PostgresDb.TableName {| Value = "purple" |} + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byContains PostgresDb.TableName {| Value = "crimson" |} + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] +] + +/// All tests for the PostgreSQL library +let all = testList "Postgres" [ + testList "Unit" [ parametersTests; queryTests ] + testSequenced <| testList "Integration" [ + configurationTests + customTests + definitionTests + documentTests + countTests + existsTests + findTests + updateTests + patchTests + removeFieldsTests + deleteTests + ] +] -- 2.45.1 From fba8ec7bc5d20137ad5d0b13f2b978cd34976d77 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 16 Aug 2024 22:04:23 -0400 Subject: [PATCH 22/29] Finish ordered tests for PostgreSQL - Rename tests, remove tested obsolete byField funcs --- src/Postgres/Extensions.fs | 80 +--- src/Tests.CSharp/CommonCSharpTests.cs | 2 +- .../PostgresCSharpExtensionTests.cs | 362 ++++++++++++++---- src/Tests.CSharp/PostgresCSharpTests.cs | 54 ++- src/Tests/PostgresExtensionTests.fs | 284 +++++++++++--- src/Tests/PostgresTests.fs | 48 ++- 6 files changed, 610 insertions(+), 220 deletions(-) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index 6492c1a..2b3b63a 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -1,6 +1,5 @@ namespace BitBadger.Documents.Postgres -open BitBadger.Documents open Npgsql open Npgsql.FSharp @@ -54,11 +53,6 @@ module Extensions = member conn.countByFields tableName howMatched fields = WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Count matching documents using a JSON field comparison query (->> =) - [] - member conn.countByField tableName field = - conn.countByFields tableName Any [ field ] - /// Count matching documents using a JSON containment query (@>) member conn.countByContains tableName criteria = WithProps.Count.byContains tableName criteria (Sql.existingConnection conn) @@ -75,11 +69,6 @@ module Extensions = member conn.existsByFields tableName howMatched fields = WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Determine if documents exist using a JSON field comparison query (->> =) - [] - member conn.existsByField tableName field = - conn.existsByFields tableName Any [ field ] - /// Determine if documents exist using a JSON containment query (@>) member conn.existsByContains tableName criteria = WithProps.Exists.byContains tableName criteria (Sql.existingConnection conn) @@ -110,11 +99,6 @@ module Extensions = WithProps.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (Sql.existingConnection conn) - /// Retrieve documents matching a JSON field comparison query (->> =) - [] - member conn.findByField<'TDoc> tableName field = - conn.findByFields<'TDoc> tableName Any [ field ] - /// Retrieve documents matching a JSON containment query (@>) member conn.findByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn) @@ -141,11 +125,6 @@ module Extensions = WithProps.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (Sql.existingConnection conn) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found - [] - member conn.findFirstByField<'TDoc> tableName field = - conn.findFirstByFields<'TDoc> tableName Any [ field ] - /// Retrieve the first document matching a JSON containment query (@>); returns None if not found member conn.findFirstByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn) @@ -180,11 +159,6 @@ module Extensions = member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - member conn.patchByField tableName field (patch: 'TPatch) = - conn.patchByFields tableName Any [ field ] patch - /// Patch documents using a JSON containment query in the WHERE clause (@>) member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = WithProps.Patch.byContains tableName criteria patch (Sql.existingConnection conn) @@ -201,11 +175,6 @@ module Extensions = member conn.removeFieldsByFields tableName howMatched fields fieldNames = WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) - /// Remove fields from documents via a comparison on a JSON field in the document - [] - member conn.removeFieldsByField tableName field fieldNames = - conn.removeFieldsByFields tableName Any [ field ] fieldNames - /// Remove fields from documents via a JSON containment query (@>) member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames = WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn) @@ -221,11 +190,6 @@ module Extensions = member conn.deleteByFields tableName howMatched fields = WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Delete documents by matching a JSON field comparison query (->> =) - [] - member conn.deleteByField tableName field = - conn.deleteByFields tableName Any [ field ] - /// Delete documents by matching a JSON containment query (@>) member conn.deleteByContains tableName (criteria: 'TContains) = WithProps.Delete.byContains tableName criteria (Sql.existingConnection conn) @@ -297,12 +261,6 @@ type NpgsqlConnectionCSharpExtensions = static member inline CountByFields(conn, tableName, howMatched, fields) = WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Count matching documents using a JSON field comparison query (->> =) - [] - [] - static member inline CountByField(conn, tableName, field) = - conn.CountByFields(tableName, Any, [ field ]) - /// Count matching documents using a JSON containment query (@>) [] static member inline CountByContains(conn, tableName, criteria: 'TCriteria) = @@ -323,12 +281,6 @@ type NpgsqlConnectionCSharpExtensions = static member inline ExistsByFields(conn, tableName, howMatched, fields) = WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Determine if documents exist using a JSON field comparison query (->> =) - [] - [] - static member inline ExistsByField(conn, tableName, field) = - conn.ExistsByFields(tableName, Any, [ field ]) - /// Determine if documents exist using a JSON containment query (@>) [] static member inline ExistsByContains(conn, tableName, criteria: 'TCriteria) = @@ -365,12 +317,6 @@ type NpgsqlConnectionCSharpExtensions = WithProps.Find.ByFieldsOrdered<'TDoc>( tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) - /// Retrieve documents matching a JSON field comparison query (->> =) - [] - [] - static member inline FindByField<'TDoc>(conn, tableName, field) = - conn.FindByFields<'TDoc>(tableName, Any, [ field ]) - /// Retrieve documents matching a JSON containment query (@>) [] static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) = @@ -404,12 +350,6 @@ type NpgsqlConnectionCSharpExtensions = WithProps.Find.FirstByFieldsOrdered<'TDoc>( tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found - [] - [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ]) - /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = @@ -453,12 +393,6 @@ type NpgsqlConnectionCSharpExtensions = static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) = WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) - /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - [] - static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = - conn.PatchByFields(tableName, Any, [ field ], patch) - /// Patch documents using a JSON containment query in the WHERE clause (@>) [] static member inline PatchByContains(conn, tableName, criteria: 'TCriteria, patch: 'TPatch) = @@ -479,17 +413,11 @@ type NpgsqlConnectionCSharpExtensions = static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) - /// Remove fields from documents via a comparison on a JSON field in the document - [] - [] - static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames) - /// Remove fields from documents via a JSON containment query (@>) [] static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) = WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn) - + /// Remove fields from documents via a JSON Path match query (@?) [] static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) = @@ -505,12 +433,6 @@ type NpgsqlConnectionCSharpExtensions = static member inline DeleteByFields(conn, tableName, howMatched, fields) = WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) - /// Delete documents by matching a JSON field comparison query (->> =) - [] - [] - static member inline DeleteByField(conn, tableName, field) = - conn.DeleteByFields(tableName, Any, [ field ]) - /// Delete documents by matching a JSON containment query (@>) [] static member inline DeleteByContains(conn, tableName, criteria: 'TContains) = diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 547e300..0dda978 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -330,7 +330,7 @@ public static class CommonCSharpTests { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", - new[] { "taco", "guac DESC", "salsa ASC" }, Dialect.SQLite), + ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", "CREATE INDEX for multiple field statement incorrect"); diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs index d23f6d3..054143a 100644 --- a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -53,7 +53,7 @@ public class PostgresCSharpExtensionTests var docs = await conn.CustomList( $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }, + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))], Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) @@ -67,7 +67,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData); + [Tuple.Create("@id", Sql.@string("one"))], Results.FromData); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc.Id, "one", "The incorrect document was returned"); }), @@ -78,7 +78,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); + [Tuple.Create("@id", Sql.@string("eighty"))], Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) ]), @@ -102,7 +102,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }); + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); @@ -119,55 +119,61 @@ public class PostgresCSharpExtensionTests { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); - var tableExists = () => conn.CustomScalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, - Results.ToExists); - var keyExists = () => conn.CustomScalar( - "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 conn.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 KeyExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", + Parameters.None, Results.ToExists); + Task TableExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", + Parameters.None, Results.ToExists); }), TestCase("EnsureDocumentIndex succeeds", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); - var indexExists = () => conn.CustomScalar( - "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 conn.EnsureTable("ensured"); await conn.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); - exists = await indexExists(); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar("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(); await using var conn = MkConn(db); - var indexExists = () => conn.CustomScalar( - "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 conn.EnsureTable("ensured"); - await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); }), TestList("Insert", [ @@ -240,17 +246,16 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountAll(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), -#pragma warning disable CS0618 - TestCase("CountByField succeeds", async () => + TestCase("CountByFields succeeds", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var theCount = await conn.CountByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + var theCount = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); Expect.equal(theCount, 2, "There should have been 2 matching documents"); }), -#pragma warning restore CS0618 TestCase("CountByContains succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -290,7 +295,6 @@ public class PostgresCSharpExtensionTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), -#pragma warning disable CS0618 TestList("ExistsByField", [ TestCase("succeeds when documents exist", async () => @@ -299,7 +303,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EX("Sub")); + var exists = await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EX("Sub")]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when documents do not exist", async () => @@ -308,11 +312,11 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); + var exists = + await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]); Expect.isFalse(exists, "There should not have been existing documents"); }) ]), -#pragma warning restore CS0618 TestList("ExistsByContains", [ TestCase("succeeds when documents exist", async () => @@ -377,6 +381,44 @@ public class PostgresCSharpExtensionTests Expect.isEmpty(results, "There should have been no documents returned"); }) ]), + TestList("FindAllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), TestList("FindById", [ TestCase("succeeds when a document is found", async () => @@ -399,8 +441,7 @@ public class PostgresCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning disable CS0618 - TestList("FindByField", + TestList("FindByFields", [ TestCase("succeeds when documents are found", async () => { @@ -408,7 +449,8 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.FindByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var docs = await conn.FindByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.equal(docs.Count, 1, "There should have been one document returned"); }), TestCase("succeeds when documents are not found", async () => @@ -417,11 +459,38 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.FindByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); + var docs = await conn.FindByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), -#pragma warning restore CS0618 + TestList("FindByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "The documents were not ordered correctly"); + }) + ]), TestList("FindByContains", [ TestCase("succeeds when documents are found", async () => @@ -444,6 +513,34 @@ public class PostgresCSharpExtensionTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), + TestList("FindByContainsOrdered", + [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|four", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar DESC")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|two", + "Documents not ordered correctly"); + }) + ]), TestList("FindByJsonPath", [ TestCase("succeeds when documents are found", async () => @@ -465,8 +562,35 @@ public class PostgresCSharpExtensionTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), -#pragma warning disable CS0618 - TestList("FindFirstByField", + TestList("FindByJsonPathOrdered", + [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "one|three|two", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue DESC")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|three|one", + "Documents not ordered correctly"); + }) + ]), + TestList("FindFirstByFields", [ TestCase("succeeds when a document is found", async () => { @@ -474,7 +598,8 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc.Id, "two", "The incorrect document was returned"); }), @@ -484,9 +609,10 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -494,11 +620,36 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning restore CS0618 + TestList("FindFirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("five", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), TestList("FindFirstByContains", [ TestCase("succeeds when a document is found", async () => @@ -520,7 +671,7 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByContains(PostgresDb.TableName, new { Sub = new { Foo = "green" } }); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -532,6 +683,31 @@ public class PostgresCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), + TestList("FindFirstByContainsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), TestList("FindFirstByJsonPath", [ TestCase("succeeds when a document is found", async () => @@ -554,7 +730,7 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -566,6 +742,31 @@ public class PostgresCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), + TestList("FindFirstByJsonPathOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), TestList("UpdateById", [ TestCase("succeeds when a document is updated", async () => @@ -650,8 +851,7 @@ public class PostgresCSharpExtensionTests await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" }); }) ]), -#pragma warning disable CS0618 - TestList("PatchByField", + TestList("PatchByFields", [ TestCase("succeeds when a document is updated", async () => { @@ -659,8 +859,10 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await conn.CountByField(PostgresDb.TableName, Field.EQ("NumValue", "77")); + await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("NumValue", "77")]); Expect.equal(after, 2, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -671,10 +873,10 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) ]), -#pragma warning restore CS0618 TestList("PatchByContains", [ TestCase("succeeds when a document is updated", async () => @@ -729,7 +931,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub", "Value" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -741,7 +943,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -754,7 +956,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["AFieldThatIsNotThere"]); }), TestCase("succeeds when no document is matched", async () => { @@ -762,11 +964,10 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Value" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Value"]); }) ]), -#pragma warning disable CS0618 - TestList("RemoveFieldsByField", + TestList("RemoveFieldsByFields", [ TestCase("succeeds when multiple fields are removed", async () => { @@ -774,8 +975,8 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -787,7 +988,8 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Sub" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -800,7 +1002,8 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Nothing" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -808,11 +1011,10 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), - new[] { "Value" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.NE("Abracadabra", "apple")], ["Value"]); }) ]), -#pragma warning restore CS0618 TestList("RemoveFieldsByContains", [ TestCase("succeeds when multiple fields are removed", async () => @@ -821,8 +1023,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -834,7 +1035,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Sub" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -847,7 +1048,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Nothing" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -855,8 +1056,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, - new[] { "Value" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, ["Value"]); }) ]), TestList("RemoveFieldsByJsonPath", @@ -867,8 +1067,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -880,7 +1079,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Sub" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -893,7 +1092,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Nothing" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -901,8 +1100,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", - new[] { "Value" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", ["Value"]); }) ]), TestList("DeleteById", @@ -928,8 +1126,7 @@ public class PostgresCSharpExtensionTests Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), -#pragma warning disable CS0618 - TestList("DeleteByField", + TestList("DeleteByFields", [ TestCase("succeeds when documents are deleted", async () => { @@ -937,7 +1134,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.DeleteByField(PostgresDb.TableName, Field.NE("Value", "purple")); + await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 2, "There should have been 2 documents remaining"); }), @@ -947,12 +1144,11 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.DeleteByField(PostgresDb.TableName, Field.EQ("Value", "crimson")); + await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) ]), -#pragma warning restore CS0618 TestList("DeleteByContains", [ TestCase("succeeds when documents are deleted", async () => diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 75335ae..93c3b4d 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -529,7 +529,7 @@ public static class PostgresCSharpTests Expect.isFalse(exists, "The index should not exist already"); await Definition.EnsureTable("ensured"); - await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); + await Definition.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); return; @@ -1043,6 +1043,29 @@ public static class PostgresCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), + TestList("FirstByContainsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), TestList("FirstByJsonPath", [ TestCase("succeeds when a document is found", async () => @@ -1073,6 +1096,29 @@ 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("FirstByJsonPathOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) ]) ]); @@ -1507,11 +1553,7 @@ public static class PostgresCSharpTests [Tests] public static readonly Test All = TestList("Postgres.C#", [ - TestList("Unit", - [ - ParametersTests, - QueryTests - ]), + TestList("Unit", [ParametersTests, QueryTests]), TestSequenced(TestList("Integration", [ ConfigurationTests, diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs index b895289..533cf12 100644 --- a/src/Tests/PostgresExtensionTests.fs +++ b/src/Tests/PostgresExtensionTests.fs @@ -7,8 +7,6 @@ open Expecto open Npgsql open Types -#nowarn "0044" - /// Open a connection to the throwaway database let private mkConn (db: ThrowawayPostgresDb) = let conn = new NpgsqlConnection(db.ConnectionString) @@ -27,7 +25,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! docs = conn.customList (Query.selectFromTable PostgresDb.TableName) [] fromData + let! docs = conn.customList (Query.find PostgresDb.TableName) [] fromData Expect.equal (List.length docs) 5 "There should have been 5 documents returned" } testTask "succeeds when data is not found" { @@ -211,12 +209,12 @@ let integrationTests = let! theCount = conn.countAll PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - testTask "countByField succeeds" { + testTask "countByFields succeeds" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! theCount = conn.countByField PostgresDb.TableName (Field.EQ "Value" "purple") + let! theCount = conn.countByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.equal theCount 2 "There should have been 2 matching documents" } testTask "countByContains succeeds" { @@ -253,13 +251,13 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "existsByField" [ + testList "existsByFields" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! exists = conn.existsByField PostgresDb.TableName (Field.EX "Sub") + let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EX "Sub" ] Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when documents do not exist" { @@ -267,7 +265,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! exists = conn.existsByField PostgresDb.TableName (Field.EQ "NumValue" "six") + let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "six" ] Expect.isFalse exists "There should not have been existing documents" } ] @@ -317,12 +315,10 @@ let integrationTests = do! conn.insert PostgresDb.TableName { Foo = "five"; Bar = "six" } let! results = conn.findAll PostgresDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" } testTask "succeeds when there is no data" { use db = PostgresDb.BuildDb() @@ -331,6 +327,44 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] + testList "findAllOrdered" [ + testTask "succeeds when ordering numerically" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "findById" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -350,13 +384,13 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] - testList "findByField" [ + testList "findByFields" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! docs = conn.findByField PostgresDb.TableName (Field.EQ "Value" "another") + let! docs = conn.findByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] Expect.equal (List.length docs) 1 "There should have been one document returned" } testTask "succeeds when documents are not found" { @@ -364,10 +398,36 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! docs = conn.findByField PostgresDb.TableName (Field.EQ "Value" "mauve") + let! docs = conn.findByFields PostgresDb.TableName Any [ Field.EQ "Value" "mauve" ] Expect.isEmpty docs "There should have been no documents returned" } ] + testList "findByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly" + } + ] testList "findByContains" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() @@ -386,6 +446,33 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] + testList "findByContainsOrdered" [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|two" "Documents not ordered correctly" + } + ] testList "findByJsonPath" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() @@ -404,13 +491,40 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] - testList "findFirstByField" [ + testList "findByJsonPathOrdered" [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "one|three|two" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue DESC" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|three|one" "Documents not ordered correctly" + } + ] + testList "findFirstByFields" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "another") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } @@ -419,7 +533,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "purple") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.isSome doc "There should have been a document returned" Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" } @@ -428,10 +542,34 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "absent") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "five" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "findFirstByContains" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -460,6 +598,30 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByContainsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "findFirstByJsonPath" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -488,6 +650,30 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByJsonPathOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "updateById" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() @@ -558,14 +744,14 @@ let integrationTests = do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |} } ] - testList "patchByField" [ + testList "patchByFields" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = conn.countByField PostgresDb.TableName (Field.EQ "NumValue" "77") + do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = conn.countByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "77" ] Expect.equal after 2 "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -575,7 +761,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] testList "patchByContains" [ @@ -625,9 +811,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -636,9 +822,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -657,16 +843,16 @@ let integrationTests = do! conn.removeFieldsById PostgresDb.TableName "two" [ "Value" ] } ] - testList "removeFieldsByField" [ + testList "removeFieldsByFields" [ testTask "succeeds when multiple fields are removed" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -674,10 +860,10 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -686,14 +872,14 @@ let integrationTests = do! loadDocs conn // This not raising an exception is the test - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Nothing" ] + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use db = PostgresDb.BuildDb() use conn = mkConn db // This not raising an exception is the test - do! conn.removeFieldsByField PostgresDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] testList "removeFieldsByContains" [ @@ -703,9 +889,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -714,9 +900,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -742,9 +928,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -753,9 +939,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -794,13 +980,13 @@ let integrationTests = Expect.equal remaining 5 "There should have been 5 documents remaining" } ] - testList "deleteByField" [ + testList "deleteByFields" [ testTask "succeeds when documents are deleted" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "purple") + do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] let! remaining = conn.countAll PostgresDb.TableName Expect.equal remaining 3 "There should have been 3 documents remaining" } @@ -809,7 +995,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "crimson") + do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = conn.countAll PostgresDb.TableName Expect.equal remaining 5 "There should have been 5 documents remaining" } diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 1c04aaa..676c8ef 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -307,7 +307,7 @@ let customTests = testList "Custom" [ use db = PostgresDb.BuildDb() do! loadDocs () - let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData + let! docs = Custom.list (Query.find PostgresDb.TableName) [] fromData Expect.hasLength docs 5 "There should have been 5 documents returned" } testTask "succeeds when data is not found" { @@ -859,6 +859,28 @@ let findTests = testList "Find" [ Expect.isNone doc "There should not have been a document returned" } ] + testList "firstByContainsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "firstByJsonPath" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -884,9 +906,31 @@ let findTests = testList "Find" [ Expect.isNone doc "There should not have been a document returned" } ] + testList "firstByJsonPathOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] ] -/// Integration tests for the Update module of the PostegreSQL library +/// Integration tests for the Update module of the PostgreSQL library let updateTests = testList "Update" [ testList "byId" [ testTask "succeeds when a document is updated" { -- 2.45.1 From d993f7178844403e9866cd34e47c6a541715c9c2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 16 Aug 2024 22:54:27 -0400 Subject: [PATCH 23/29] Reorg SQLite tests --- src/Tests.CSharp/SqliteCSharpTests.cs | 1283 ++++++++++++------------ src/Tests/SqliteTests.fs | 1322 +++++++++++++------------ 2 files changed, 1348 insertions(+), 1257 deletions(-) diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index e0c534e..13cc1f3 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -1,6 +1,5 @@ using Expecto.CSharp; using Expecto; -using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; using BitBadger.Documents.Sqlite; @@ -14,123 +13,125 @@ using static Runner; public static class SqliteCSharpTests { /// - /// Unit tests for the SQLite library + /// Unit tests for the Query module of the SQLite library /// - private static readonly Test Unit = TestList("Unit", + private static readonly Test QueryTests = TestList("Query", [ - TestList("Query", + TestList("WhereByFields", [ - TestList("WhereByFields", - [ - TestCase("succeeds for a single field when a logical operator is passed", () => - { - Expect.equal( - Sqlite.Query.WhereByFields(FieldMatch.Any, - [Field.GT("theField", 0).WithParameterName("@test")]), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds for a single field when an existence operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), - "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(FieldMatch.All, - [Field.BT("aField", 50, 99).WithParameterName("@range")]), - "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); - }), - TestCase("succeeds for all multiple fields with logical operators", () => - { - Expect.equal( - Sqlite.Query.WhereByFields(FieldMatch.All, - [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), - "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(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]), - "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(FieldMatch.All, - [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), - "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", - "WHERE clause not correct"); - }) - ]), - TestCase("WhereById succeeds", () => + TestCase("succeeds for a single field when a logical operator is passed", () => { - Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.GT("theField", 0).WithParameterName("@test")]), + "data->>'theField' > @test", "WHERE clause not correct"); }), - TestCase("Patch succeeds", () => + TestCase("succeeds for a single field when an existence operator is passed", () => { - Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName), - $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct"); + Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), + "data->>'thatField' IS NULL", "WHERE clause not correct"); }), - TestCase("RemoveFields succeeds", () => + TestCase("succeeds for a single field when a between operator is passed", () => { - Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]), - $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)", - "Field removal query not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99).WithParameterName("@range")]), + "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); }), - TestCase("ById succeeds", () => + TestCase("succeeds for all multiple fields with logical operators", () => { - Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", - "By-ID query not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); }), - TestCase("ByFields succeeds", () => + TestCase("succeeds for any multiple fields with an existence operator", () => { - Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), - "unit WHERE data->>'That' > @field0", "By-Field query not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]), + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct"); }), - TestCase("Definition.EnsureTable succeeds", () => + TestCase("succeeds for all multiple fields with between operators", () => { - Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), - "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); }) ]), - TestList("Parameters", - [ - TestCase("Id succeeds", () => - { - var theParam = Parameters.Id(7); - Expect.equal(theParam.ParameterName, "@id", "The parameter name is incorrect"); - Expect.equal(theParam.Value, "7", "The parameter value is incorrect"); - }), - TestCase("Json succeeds", () => - { - var theParam = Parameters.Json("@test", new { Nice = "job" }); - Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect"); - Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect"); - }), -#pragma warning disable CS0618 - TestCase("AddField succeeds when adding a parameter", () => - { - 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"); - Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); - }), - TestCase("AddField succeeds when not adding a parameter", () => - { - var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []); - Expect.isEmpty(paramSeq, "There should not have been any parameters added"); - }), -#pragma warning restore CS0618 - TestCase("None succeeds", () => - { - Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); - }) - ]) - // Results are exhaustively executed in the context of other tests + TestCase("WhereById succeeds", () => + { + Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); + }), + TestCase("Patch succeeds", () => + { + Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName), + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]), + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)", + "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE data->>'That' > @field0", "By-Field query not correct"); + }), + TestCase("Definition.EnsureTable succeeds", () => + { + Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); + }) ]); + /// + /// Unit tests for the Parameters module of the SQLite library + /// + private static readonly Test ParametersTests = TestList("Parameters", + [ + TestCase("Id succeeds", () => + { + var theParam = Parameters.Id(7); + Expect.equal(theParam.ParameterName, "@id", "The parameter name is incorrect"); + Expect.equal(theParam.Value, "7", "The parameter value is incorrect"); + }), + TestCase("Json succeeds", () => + { + var theParam = Parameters.Json("@test", new { Nice = "job" }); + Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect"); + Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect"); + }), +#pragma warning disable CS0618 + TestCase("AddField succeeds when adding a parameter", () => + { + 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"); + Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); + }), + TestCase("AddField succeeds when not adding a parameter", () => + { + var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []); + Expect.isEmpty(paramSeq, "There should not have been any parameters added"); + }), +#pragma warning restore CS0618 + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); + }) + ]); + + // Results are exhaustively executed in the context of other tests + + /// + /// Documents used for integration tests + /// private static readonly List TestDocuments = [ new() { Id = "one", Value = "FIRST!", NumValue = 0 }, @@ -148,144 +149,158 @@ public static class SqliteCSharpTests foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } - private static readonly Test Integration = TestList("Integration", - [ - TestCase("Configuration.UseConnectionString succeeds", () => + /// + /// Integration tests for the Configuration module of the SQLite library + /// + private static readonly Test ConfigurationTests = TestCase("Configuration.UseConnectionString succeeds", () => + { + try { - try + Sqlite.Configuration.UseConnectionString("Data Source=test.db"); + Expect.equal(Sqlite.Configuration.connectionString, + new FSharpOption("Data Source=test.db;Foreign Keys=True"), "Connection string incorrect"); + } + finally + { + Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); + } + }); + + /// + /// Integration tests for the Custom module of the SQLite library + /// + private static readonly Test CustomTests = TestList("Custom", + [ + TestList("Single", + [ + TestCase("succeeds when a row is found", async () => { - Sqlite.Configuration.UseConnectionString("Data Source=test.db"); - Expect.equal(Sqlite.Configuration.connectionString, - new FSharpOption("Data Source=test.db;Foreign Keys=True"), "Connection string incorrect"); - } - finally + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", + [Parameters.Id("one")], Results.FromData); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when a row is not found", async () => { - Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", + [Parameters.Id("eighty")], Results.FromData); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("List", + [ + TestCase("succeeds when data is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List(Query.Find(SqliteDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); + }), + TestCase("succeeds when data is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List( + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", [new("@value", 100)], + Results.FromData); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("NonQuery", + [ + TestCase("succeeds when operating on data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); + + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); + }), + TestCase("succeeds when no data matches where clause", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", + [new("@value", 100)]); + + 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(); + + 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"); + }) + ]); + + /// + /// Integration tests for the Definition module of the SQLite library + /// + private static readonly Test DefinitionTests = TestList("Definition", + [ + TestCase("EnsureTable succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var exists = await ItExists("ensured"); + var alsoExists = await ItExists("idx_ensured_key"); + 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 ItExists("ensured"); + alsoExists = await ItExists("idx_ensured_key"); + Expect.isTrue(exists, "The table should now exist"); + Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + async ValueTask ItExists(string name) + { + return await Custom.Scalar($"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + [new("@name", name)], Results.ToExists); } }), - TestList("Custom", - [ - TestList("Single", - [ - TestCase("succeeds when a row is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + TestCase("EnsureFieldIndex succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); - var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("one") }, Results.FromData); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when a row is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + var exists = await IndexExists(); + Expect.isFalse(exists, "The index should not exist already"); - var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("eighty") }, Results.FromData); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("List", - [ - TestCase("succeeds when data is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await Definition.EnsureTable("ensured"); + await Definition.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); + Expect.isTrue(exists, "The index should now exist"); + return; - var docs = await Custom.List(Query.Find(SqliteDb.TableName), Parameters.None, - Results.FromData); - Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); - }), - TestCase("succeeds when data is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + Task IndexExists() => Custom.Scalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); + }) + ]); - var docs = await Custom.List( - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }, Results.FromData); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("NonQuery", - [ - TestCase("succeeds when operating on data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); - - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); - }), - TestCase("succeeds when no data matches where clause", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }); - - 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(); - - 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", - [ - TestCase("EnsureTable succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var exists = await ItExists("ensured"); - var alsoExists = await ItExists("idx_ensured_key"); - 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 ItExists("ensured"); - alsoExists = await ItExists("idx_ensured_key"); - Expect.isTrue(exists, "The table should now exist"); - Expect.isTrue(alsoExists, "The key index should now exist"); - return; - - async ValueTask ItExists(string name) - { - return await Custom.Scalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, Results.ToExists); - } - }), - TestCase("EnsureFieldIndex succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - - 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(); - 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", + /// + /// Integration tests for the Document module of the SQLite library + /// + private static readonly Test DocumentTests = TestList("Document", + [ + TestList("Insert", [ TestCase("succeeds", async () => { @@ -344,401 +359,443 @@ 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", + ]) + ]); + + /// + /// Integration tests for the Count module of the SQLite library + /// + private static readonly Test CountTests = TestList("Count", + [ + TestCase("All succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.All(SqliteDb.TableName); + Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + }), + TestList("ByFields", [ - TestCase("All succeeds", async () => + TestCase("succeeds for numeric range", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var theCount = await Count.All(SqliteDb.TableName); - Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.BT("NumValue", 10, 20)]); + Expect.equal(theCount, 3L, "There should have been 3 matching documents"); }), - TestList("ByFields", - [ - TestCase("succeeds for numeric range", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + TestCase("succeeds for non-numeric range", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.BT("NumValue", 10, 20)]); - Expect.equal(theCount, 3L, "There should have been 3 matching documents"); - }), - TestCase("succeeds for non-numeric range", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.BT("Value", "aardvark", "apple")]); - Expect.equal(theCount, 1L, "There should have been 1 matching document"); - }) - ]) - ]), - TestList("Exists", - [ - TestList("ById", - [ - TestCase("succeeds when a document exists", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(SqliteDb.TableName, "three"); - Expect.isTrue(exists, "There should have been an existing document"); - }), - TestCase("succeeds when a document does not exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(SqliteDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.EQ("Nothing", "none")]); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - ]) - ]), - TestList("Find", - [ - TestList("All", - [ - TestCase("succeeds when there is data", async () => - { - await using var db = await SqliteDb.BuildDb(); - - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); - - var results = await Find.All(SqliteDb.TableName); - Expect.equal(results.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when there is no data", async () => - { - await using var db = await SqliteDb.BuildDb(); - var results = await Find.All(SqliteDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) - ]), - TestList("ById", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(SqliteDb.TableName, "two"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(SqliteDb.TableName, "twenty two"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents are found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.GT("NumValue", 15)]); - Expect.equal(docs.Count, 2, "There should have been two documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "mauve")]); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - ]), - TestList("FirstByFields", - [ - TestCase("succeeds when a document is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "another")]); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.EQ("Sub.Foo", "green")]); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, - [Field.EQ("Value", "absent")]); - Expect.isNull(doc, "There should not have been a document returned"); - }) - ]) - ]), - TestList("Update", - [ - TestList("ById", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await Update.ById(SqliteDb.TableName, "one", testDoc); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is not correct"); - Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); - Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct"); - Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ById(SqliteDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) - ]), - TestList("ByFunc", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is incorrect"); - Expect.equal(after.Value, "le un", "The updated document is incorrect"); - Expect.equal(after.NumValue, 1, "The updated document is incorrect"); - Expect.isNull(after.Sub, "The updated document should not have a sub-document"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - ]), - ]), - TestList("Patch", - [ - TestList("ById", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Patch.ById(SqliteDb.TableName, "one", new { NumValue = 44 }); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is not correct"); - Expect.equal(after.NumValue, 44, "The updated document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], - new { NumValue = 77 }); - var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); - Expect.equal(after, 2L, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], - new { Foo = "green" }); - }) - ]) - ]), - TestList("RemoveFields", - [ - TestList("ById", - [ - TestCase("succeeds when fields are removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Sub", "Value" }); - var updated = await Find.ById(SqliteDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = await SqliteDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when a field is removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], - new[] { "Sub" }); - var updated = await Find.ById(SqliteDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], - new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = await SqliteDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], - new[] { "Value" }); - }) - ]) - ]), - TestList("Delete", - [ - TestList("ById", - [ - TestCase("succeeds when a document is deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(SqliteDb.TableName, "four"); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); - }), - TestCase("succeeds when a document is not deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(SqliteDb.TableName, "thirty"); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) - ]), - TestList("ByFields", - [ - TestCase("succeeds when documents are deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.EQ("Value", "crimson")]); - 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:")) + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.BT("Value", "aardvark", "apple")]); + Expect.equal(theCount, 1L, "There should have been 1 matching document"); + }) + ]) ]); + /// + /// Integration tests for the Exists module of the SQLite library + /// + private static readonly Test ExistsTests = TestList("Exists", + [ + TestList("ById", + [ + TestCase("succeeds when a document exists", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(SqliteDb.TableName, "three"); + Expect.isTrue(exists, "There should have been an existing document"); + }), + TestCase("succeeds when a document does not exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(SqliteDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when no matching documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + ]) + ]); + + /// + /// Integration tests for the Find module of the SQLite library + /// + private static readonly Test FindTests = TestList("Find", + [ + TestList("All", + [ + TestCase("succeeds when there is data", async () => + { + await using var db = await SqliteDb.BuildDb(); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + + var results = await Find.All(SqliteDb.TableName); + Expect.equal(results.Count, 3, "There should have been 3 documents returned"); + }), + TestCase("succeeds when there is no data", async () => + { + await using var db = await SqliteDb.BuildDb(); + var results = await Find.All(SqliteDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + ]), + TestList("ById", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(SqliteDb.TableName, "two"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(SqliteDb.TableName, "twenty two"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)]); + Expect.equal(docs.Count, 2, "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("FirstByFields", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]) + ]); + + /// + /// Integration tests for the Update module of the SQLite library + /// + private static readonly Test UpdateTests = TestList("Update", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await Update.ById(SqliteDb.TableName, "one", testDoc); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is not correct"); + Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); + Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct"); + Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ById(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + ]), + TestList("ByFunc", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is incorrect"); + Expect.equal(after.Value, "le un", "The updated document is incorrect"); + Expect.equal(after.NumValue, 1, "The updated document is incorrect"); + Expect.isNull(after.Sub, "The updated document should not have a sub-document"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + ]), + ]); + + /// + /// Integration tests for the Patch module of the SQLite library + /// + private static readonly Test PatchTests = TestList("Patch", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Patch.ById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is not correct"); + Expect.equal(after.NumValue, 44, "The updated document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); + Expect.equal(after, 2L, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); + }) + ]) + ]); + + /// + /// Integration tests for the RemoveFields module of the SQLite library + /// + private static readonly Test RemoveFieldsTests = TestList("RemoveFields", + [ + TestList("ById", + [ + TestCase("succeeds when fields are removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(SqliteDb.TableName, "two", ["Sub", "Value"]); + var updated = await Find.ById(SqliteDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ById(SqliteDb.TableName, "two", ["AFieldThatIsNotThere"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = await SqliteDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ById(SqliteDb.TableName, "two", ["Value"]); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a field is removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], ["Sub"]); + var updated = await Find.ById(SqliteDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = await SqliteDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); + }) + ]) + ]); + + /// + /// Integration tests for the Delete module of the SQLite library + /// + private static readonly Test DeleteTests = TestList("Delete", + [ + TestList("ById", + [ + TestCase("succeeds when a document is deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(SqliteDb.TableName, "four"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); + }), + TestCase("succeeds when a document is not deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(SqliteDb.TableName, "thirty"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.EQ("Value", "crimson")]); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + ]) + ]); + /// /// All tests for SQLite C# functions and methods /// [Tests] - public static readonly Test All = TestList("Sqlite.C#", [Unit, TestSequenced(Integration)]); + public static readonly Test All = TestList("Sqlite.C#", + [ + TestList("Unit", [QueryTests, ParametersTests]), + TestSequenced(TestList("Integration", + [ + ConfigurationTests, + CustomTests, + DefinitionTests, + DocumentTests, + CountTests, + ExistsTests, + FindTests, + UpdateTests, + PatchTests, + RemoveFieldsTests, + DeleteTests, + TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) + ])) + ]); } diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index c6a0ee3..1006e25 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -10,651 +10,685 @@ open Types #nowarn "0044" -/// Unit tests for the SQLite library -let unitTests = - testList "Unit" [ - testList "Query" [ - testList "whereByFields" [ - test "succeeds for a single field when a logical operator is passed" { - Expect.equal - (Query.whereByFields Any [ { Field.GT "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" { - Expect.equal - (Query.whereByFields Any [ Field.NEX "thatField" ]) - "data->>'thatField' IS NULL" - "WHERE clause not correct" - } - test "succeeds for a single field when a between operator is passed" { - Expect.equal - (Query.whereByFields All [ { Field.BT "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" { - Expect.equal - (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) - "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 Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) - "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 All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) - "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" - "WHERE clause not correct" - } - ] - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" - } - test "patch succeeds" { - Expect.equal - (Query.patch SqliteDb.TableName) - $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))" - "Patch query not correct" - } - test "removeFields succeeds" { - Expect.equal - (Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ]) - $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)" - "Field removal query not correct" - } - test "byId succeeds" { - Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" - } - test "byFields succeeds" { - Expect.equal - (Query.byFields "unit" Any [ Field.GT "That" 14 ]) - "unit WHERE data->>'That' > @field0" - "By-Field query not correct" - } - test "Definition.ensureTable succeeds" { - Expect.equal - (Query.Definition.ensureTable "tbl") - "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" - "CREATE TABLE statement not correct" - } - ] - testList "Parameters" [ - test "idParam succeeds" { - let theParam = idParam 7 - Expect.equal theParam.ParameterName "@id" "The parameter name is incorrect" - Expect.equal theParam.Value "7" "The parameter value is incorrect" - } - test "jsonParam succeeds" { - let theParam = jsonParam "@test" {| Nice = "job" |} - Expect.equal theParam.ParameterName "@test" "The parameter name is incorrect" - Expect.equal theParam.Value """{"Nice":"job"}""" "The parameter value is incorrect" - } - testList "addFieldParam" [ - test "succeeds when adding a parameter" { - let paramList = addFieldParam "@field" (Field.EQ "it" 99) [] - Expect.hasLength paramList 1 "There should have been a parameter added" - let theParam = Seq.head paramList - Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" - Expect.equal theParam.Value 99 "The parameter value is incorrect" - } - test "succeeds when not adding a parameter" { - let paramList = addFieldParam "@it" (Field.NEX "Coffee") [] - Expect.isEmpty paramList "There should not have been any parameters added" - } - ] - test "noParams succeeds" { - Expect.isEmpty noParams "The parameter list should have been empty" - } - ] - // Results are exhaustively executed in the context of other tests - ] +(** UNIT TESTS **) -/// These tests each use a fresh copy of a SQLite database -let integrationTests = - let documents = [ - { 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 loadDocs () = backgroundTask { - for doc in documents do do! insert SqliteDb.TableName doc - } - testList "Integration" [ - testList "Configuration" [ - test "useConnectionString / connectionString succeed" { - try - Configuration.useConnectionString "Data Source=test.db" - Expect.equal - Configuration.connectionString - (Some "Data Source=test.db;Foreign Keys=True") - "Connection string incorrect" - finally - Configuration.useConnectionString "Data Source=:memory:" - } - test "useSerializer succeeds" { - try - Configuration.useSerializer - { new IDocumentSerializer with - member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" - member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> - } - - let serialized = Configuration.serializer().Serialize { Foo = "howdy"; Bar = "bye"} - Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" - - let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" - Expect.isNull deserialized "Specified serializer should have returned null" - finally - Configuration.useSerializer DocumentSerializer.``default`` - } - test "serializer returns configured serializer" { - Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) - "Serializer should have been the same" - } - test "useIdField / idField succeeds" { - Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" - Configuration.useIdField "id" - Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" - Configuration.useIdField "Id" - } - ] - testList "Custom" [ - testList "single" [ - testTask "succeeds when a row is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" - [ SqliteParameter("@id", "one") ] - fromData - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when a row is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" - [ SqliteParameter("@id", "eighty") ] - fromData - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "list" [ - testTask "succeeds when data is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Custom.list (Query.selectFromTable SqliteDb.TableName) [] fromData - Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" - } - testTask "succeeds when data is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = - Custom.list - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - fromData - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "nonQuery" [ - testTask "succeeds when operating on data" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" [] - - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 0L "There should be no documents remaining in the table" - } - testTask "succeeds when no data matches where clause" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery - $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should be 5 documents remaining in the table" - } - ] - testTask "scalar succeeds" { - use! db = SqliteDb.BuildDb() - - let! nbr = Custom.scalar "SELECT 5 AS test_value" [] _.GetInt32(0) - Expect.equal nbr 5 "The query should have returned the number 5" - } - ] - testList "Definition" [ - testTask "ensureTable succeeds" { - use! db = SqliteDb.BuildDb() - let itExists (name: string) = - Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" - [ SqliteParameter("@name", name) ] - toExists - - let! exists = itExists "ensured" - let! alsoExists = itExists "idx_ensured_key" - Expect.isFalse exists "The table should not exist already" - Expect.isFalse alsoExists "The key index should not exist already" - - do! Definition.ensureTable "ensured" - let! exists' = itExists "ensured" - let! alsoExists' = itExists "idx_ensured_key" - Expect.isTrue exists' "The table should now exist" - Expect.isTrue alsoExists' "The key index should now exist" - } - testTask "ensureFieldIndex succeeds" { - use! db = SqliteDb.BuildDb() - let indexExists () = - Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it" - [] - toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ] - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - ] - testList "insert" [ - testTask "succeeds" { - use! db = SqliteDb.BuildDb() - let! before = Find.all SqliteDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } - do! insert SqliteDb.TableName testDoc - let! after = Find.all SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "fails for duplicate key" { - use! db = SqliteDb.BuildDb() - do! insert SqliteDb.TableName { emptyDoc with Id = "test" } - Expect.throws - (fun () -> - insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) - "An exception should have been raised for duplicate document ID insert" - } - ] - testList "save" [ - testTask "succeeds when a document is inserted" { - use! db = SqliteDb.BuildDb() - let! before = Find.all SqliteDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! save SqliteDb.TableName testDoc - let! after = Find.all SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! insert SqliteDb.TableName testDoc - - let! before = Find.byId SqliteDb.TableName "test" - Expect.isSome before "There should have been a document returned" - Expect.equal before.Value testDoc "The document is not correct" - - let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } - do! save SqliteDb.TableName upd8Doc - let! after = Find.byId SqliteDb.TableName "test" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value upd8Doc "The updated document is not correct" - } - ] - testList "Count" [ - testTask "all succeeds" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.all SqliteDb.TableName - Expect.equal theCount 5L "There should have been 5 matching documents" - } - testList "byFields" [ - testTask "succeeds for a numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "NumValue" 10 20 ] - Expect.equal theCount 3L "There should have been 3 matching documents" - } - testTask "succeeds for a non-numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "Value" "aardvark" "apple" ] - Expect.equal theCount 1L "There should have been 1 matching document" - } - ] - ] - testList "Exists" [ - testList "byId" [ - testTask "succeeds when a document exists" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId SqliteDb.TableName "three" - Expect.isTrue exists "There should have been an existing document" - } - testTask "succeeds when a document does not exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId SqliteDb.TableName "seven" - Expect.isFalse exists "There should not have been an existing document" - } - ] - testList "byFields" [ - testTask "succeeds when documents exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byFields SqliteDb.TableName Any [ Field.LT "Nothing" "none" ] - Expect.isFalse exists "There should not have been any existing documents" - } - ] - ] - testList "Find" [ - testList "all" [ - testTask "succeeds when there is data" { - use! db = SqliteDb.BuildDb() - - do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" } - do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" } - do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" } - - let! results = Find.all SqliteDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" - } - testTask "succeeds when there is no data" { - use! db = SqliteDb.BuildDb() - let! results = Find.all SqliteDb.TableName - Expect.equal results [] "There should have been no documents returned" - } - ] - testList "byId" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId SqliteDb.TableName "two" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId SqliteDb.TableName "three hundred eighty-seven" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" - } - ] - testList "byFields" [ - testTask "succeeds when documents are found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] - Expect.equal (List.length docs) 2 "There should have been two documents returned" - } - testTask "succeeds when documents are not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 100 ] - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" - } - ] - testList "firstByFields" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" - } - ] - ] - testList "Update" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! Update.byId SqliteDb.TableName "one" testDoc - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value testDoc "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byId - SqliteDb.TableName - "test" - { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } - } - ] - testList "byFunc" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Update.byFunc - SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal - after.Value - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byFunc - SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - } - ] - ] - testList "Patch" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Patch.byId SqliteDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value.NumValue 44 "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} - } - ] - testList "byFields" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} - let! after = Count.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] - Expect.equal after 2L "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} - } - ] - ] - testList "RemoveFields" [ - testList "byId" [ - testTask "succeeds when fields is removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId SqliteDb.TableName "two" [ "Sub"; "Value" ] - try - let! _ = Find.byId SqliteDb.TableName "two" - Expect.isTrue false "The updated document should have failed to parse" - with - | :? JsonException -> () - | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" - } - testTask "succeeds when a field is not removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byId SqliteDb.TableName "two" [ "AFieldThatIsNotThere" ] - } - testTask "succeeds when no document is matched" { - use! db = SqliteDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byId SqliteDb.TableName "two" [ "Value" ] - } - ] - testList "byFields" [ - testTask "succeeds when a field is removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] - try - let! _ = Find.byId SqliteDb.TableName "four" - Expect.isTrue false "The updated document should have failed to parse" - with - | :? JsonException -> () - | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" - } - testTask "succeeds when a field is not removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use! db = SqliteDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] - } - ] - ] - testList "Delete" [ - testList "byId" [ - testTask "succeeds when a document is deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byId SqliteDb.TableName "four" - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 4L "There should have been 4 documents remaining" - } - testTask "succeeds when a document is not deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byId SqliteDb.TableName "thirty" - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - testList "byFields" [ - testTask "succeeds when documents are deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 2L "There should have been 2 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - ] - test "clean up database" { - Configuration.useConnectionString "data source=:memory:" +/// 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" { + Expect.equal + (Query.whereByFields Any [ { Field.GT "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" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField" ]) + "data->>'thatField' IS NULL" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed" { + Expect.equal + (Query.whereByFields All [ { Field.BT "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" { + Expect.equal + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) + "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 Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) + "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 All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" } ] - |> testSequenced + test "whereById succeeds" { + Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" + } + test "patch succeeds" { + Expect.equal + (Query.patch SqliteDb.TableName) + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ]) + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE data->>'That' > @field0" + "By-Field query not correct" + } + test "Definition.ensureTable succeeds" { + Expect.equal + (Query.Definition.ensureTable "tbl") + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" + "CREATE TABLE statement not correct" + } +] -let all = testList "Sqlite" [ unitTests; integrationTests ] +/// Unit tests for the Parameters module of the SQLite library +let parametersTests = testList "Parameters" [ + test "idParam succeeds" { + let theParam = idParam 7 + Expect.equal theParam.ParameterName "@id" "The parameter name is incorrect" + Expect.equal theParam.Value "7" "The parameter value is incorrect" + } + test "jsonParam succeeds" { + let theParam = jsonParam "@test" {| Nice = "job" |} + Expect.equal theParam.ParameterName "@test" "The parameter name is incorrect" + Expect.equal theParam.Value """{"Nice":"job"}""" "The parameter value is incorrect" + } + testList "addFieldParam" [ + test "succeeds when adding a parameter" { + let paramList = addFieldParam "@field" (Field.EQ "it" 99) [] + Expect.hasLength paramList 1 "There should have been a parameter added" + let theParam = Seq.head paramList + Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" + Expect.equal theParam.Value 99 "The parameter value is incorrect" + } + test "succeeds when not adding a parameter" { + let paramList = addFieldParam "@it" (Field.NEX "Coffee") [] + Expect.isEmpty paramList "There should not have been any parameters added" + } + ] + test "noParams succeeds" { + Expect.isEmpty noParams "The parameter list should have been empty" + } +] +// Results are exhaustively executed in the context of other tests + + +(** INTEGRATION TESTS **) + +/// Documents used for integration tests +let documents = [ + { 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 } +] + +/// Load a table with the test documents +let loadDocs () = backgroundTask { + for doc in documents do do! insert SqliteDb.TableName doc +} + +/// Integration tests for the Configuration module of the SQLite library +let configurationTests = testList "Configuration" [ + test "useConnectionString / connectionString succeed" { + try + Configuration.useConnectionString "Data Source=test.db" + Expect.equal + Configuration.connectionString + (Some "Data Source=test.db;Foreign Keys=True") + "Connection string incorrect" + finally + Configuration.useConnectionString "Data Source=:memory:" + } + test "useSerializer succeeds" { + try + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" + member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> + } + + let serialized = Configuration.serializer().Serialize { Foo = "howdy"; Bar = "bye"} + Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" + + let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" + Expect.isNull deserialized "Specified serializer should have returned null" + finally + Configuration.useSerializer DocumentSerializer.``default`` + } + test "serializer returns configured serializer" { + Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) + "Serializer should have been the same" + } + test "useIdField / idField succeeds" { + Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" + Configuration.useIdField "id" + Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" + Configuration.useIdField "Id" + } +] + +/// Integration tests for the Custom module of the SQLite library +let customTests = testList "Custom" [ + testList "single" [ + testTask "succeeds when a row is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" + [ SqliteParameter("@id", "one") ] + fromData + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when a row is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" + [ SqliteParameter("@id", "eighty") ] + fromData + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "list" [ + testTask "succeeds when data is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Custom.list (Query.find SqliteDb.TableName) [] fromData + Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Custom.list + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "nonQuery" [ + testTask "succeeds when operating on data" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" [] + + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 0L "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use! db = SqliteDb.BuildDb() + + let! nbr = Custom.scalar "SELECT 5 AS test_value" [] _.GetInt32(0) + Expect.equal nbr 5 "The query should have returned the number 5" + } +] + +/// Integration tests for the Definition module of the SQLite library +let definitionTests = testList "Definition" [ + testTask "ensureTable succeeds" { + use! db = SqliteDb.BuildDb() + let itExists (name: string) = + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" + [ SqliteParameter("@name", name) ] + toExists + + let! exists = itExists "ensured" + let! alsoExists = itExists "idx_ensured_key" + Expect.isFalse exists "The table should not exist already" + Expect.isFalse alsoExists "The key index should not exist already" + + do! Definition.ensureTable "ensured" + let! exists' = itExists "ensured" + let! alsoExists' = itExists "idx_ensured_key" + Expect.isTrue exists' "The table should now exist" + Expect.isTrue alsoExists' "The key index should now exist" + } + testTask "ensureFieldIndex succeeds" { + use! db = SqliteDb.BuildDb() + let indexExists () = + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it" + [] + toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } +] + +/// Integration tests for the (auto-opened) Document module of the SQLite library +let documentTests = testList "Document" [ + testList "insert" [ + testTask "succeeds" { + use! db = SqliteDb.BuildDb() + let! before = Find.all SqliteDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } + do! insert SqliteDb.TableName testDoc + let! after = Find.all SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use! db = SqliteDb.BuildDb() + do! insert SqliteDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) + "An exception should have been raised for duplicate document ID insert" + } + ] + testList "save" [ + testTask "succeeds when a document is inserted" { + use! db = SqliteDb.BuildDb() + let! before = Find.all SqliteDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! save SqliteDb.TableName testDoc + let! after = Find.all SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! insert SqliteDb.TableName testDoc + + let! before = Find.byId SqliteDb.TableName "test" + Expect.isSome before "There should have been a document returned" + Expect.equal before.Value testDoc "The document is not correct" + + let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } + do! save SqliteDb.TableName upd8Doc + let! after = Find.byId SqliteDb.TableName "test" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] +] + +/// Integration tests for the Count module of the SQLite library +let countTests = testList "Count" [ + testTask "all succeeds" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.all SqliteDb.TableName + Expect.equal theCount 5L "There should have been 5 matching documents" + } + testList "byFields" [ + testTask "succeeds for a numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "NumValue" 10 20 ] + Expect.equal theCount 3L "There should have been 3 matching documents" + } + testTask "succeeds for a non-numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "Value" "aardvark" "apple" ] + Expect.equal theCount 1L "There should have been 1 matching document" + } + ] +] + +/// Integration tests for the Exists module of the SQLite library +let existsTests = testList "Exists" [ + testList "byId" [ + testTask "succeeds when a document exists" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId SqliteDb.TableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId SqliteDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "byFields" [ + testTask "succeeds when documents exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.LT "Nothing" "none" ] + Expect.isFalse exists "There should not have been any existing documents" + } + ] +] + +/// Integration tests for the Find module of the SQLite library +let findTests = testList "Find" [ + testList "all" [ + testTask "succeeds when there is data" { + use! db = SqliteDb.BuildDb() + + do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" } + do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" } + do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" } + + let! results = Find.all SqliteDb.TableName + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" + } + testTask "succeeds when there is no data" { + use! db = SqliteDb.BuildDb() + let! results = Find.all SqliteDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "byId" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId SqliteDb.TableName "two" + Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId SqliteDb.TableName "three hundred eighty-seven" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + testList "byFields" [ + testTask "succeeds when documents are found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 100 ] + Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + } + ] + testList "firstByFields" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] +] + +/// Integration tests for the Update module of the SQLite library +let updateTests = testList "Update" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! Update.byId SqliteDb.TableName "one" testDoc + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value testDoc "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byId + SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "byFunc" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Update.byFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal + after.Value + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] +] + +/// Integration tests for the Patch module of the SQLite library +let patchTests = testList "Patch" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Patch.byId SqliteDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value.NumValue 44 "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} + } + ] + testList "byFields" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] + Expect.equal after 2L "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} + } + ] +] + +/// Integration tests for the RemoveFields module of the SQLite library +let removeFieldsTests = testList "RemoveFields" [ + testList "byId" [ + testTask "succeeds when fields is removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId SqliteDb.TableName "two" [ "Sub"; "Value" ] + try + let! _ = Find.byId SqliteDb.TableName "two" + Expect.isTrue false "The updated document should have failed to parse" + with + | :? JsonException -> () + | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" + } + testTask "succeeds when a field is not removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byId SqliteDb.TableName "two" [ "AFieldThatIsNotThere" ] + } + testTask "succeeds when no document is matched" { + use! db = SqliteDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byId SqliteDb.TableName "two" [ "Value" ] + } + ] + testList "byFields" [ + testTask "succeeds when a field is removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] + try + let! _ = Find.byId SqliteDb.TableName "four" + Expect.isTrue false "The updated document should have failed to parse" + with + | :? JsonException -> () + | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" + } + testTask "succeeds when a field is not removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use! db = SqliteDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] + } + ] +] + +/// Integration tests for the Delete module of the SQLite library +let deleteTests = testList "Delete" [ + testList "byId" [ + testTask "succeeds when a document is deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byId SqliteDb.TableName "four" + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 4L "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byId SqliteDb.TableName "thirty" + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "byFields" [ + testTask "succeeds when documents are deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 2L "There should have been 2 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] +] + +/// All tests for the SQLite library +let all = testList "Sqlite" [ + testList "Unit" [ queryTests; parametersTests ] + testSequenced <| testList "Integration" [ + configurationTests + customTests + definitionTests + documentTests + countTests + existsTests + findTests + updateTests + patchTests + removeFieldsTests + deleteTests + test "clean up database" { Configuration.useConnectionString "data source=:memory:" } + ] +] -- 2.45.1 From e2948ea6a1e44c76b4483d8386d3df664e17b228 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 17 Aug 2024 12:38:14 -0400 Subject: [PATCH 24/29] Add ordered* tests for SQLite - Ignore "n:" prefix for SQLite fields - Remove byFIeld functions --- src/Common/Library.fs | 10 +- src/Sqlite/Extensions.fs | 73 +------ src/Sqlite/Library.fs | 2 +- src/Tests.CSharp/CommonCSharpTests.cs | 32 ++- .../SqliteCSharpExtensionTests.cs | 206 +++++++++++++----- src/Tests.CSharp/SqliteCSharpTests.cs | 79 +++++++ src/Tests/CommonTests.fs | 24 +- src/Tests/SqliteExtensionTests.fs | 152 ++++++++++--- src/Tests/SqliteTests.fs | 89 +++++++- 9 files changed, 474 insertions(+), 193 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index fd2554b..4a9f171 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -309,10 +309,10 @@ module Query = { it with Name = parts[0] }, Some $" {parts[1]}" else it, None) |> Seq.map (fun (field, direction) -> - let path = - if dialect = PostgreSQL && field.Name.StartsWith "n:" then - $"({ { field with Name = field.Name[2..] }.Path dialect})::numeric" - else field.Path dialect - path + defaultArg direction "") + match dialect, field.Name.StartsWith "n:" with + | PostgreSQL, true -> $"({ { field with Name = field.Name[2..] }.Path PostgreSQL})::numeric" + | SQLite, true -> { field with Name = field.Name[2..] }.Path SQLite + | _, _ -> field.Path dialect + |> function path -> path + defaultArg direction "") |> String.concat ", " |> function it -> $" ORDER BY {it}" diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index c5e83e5..d8b5106 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -49,11 +49,6 @@ module Extensions = member conn.countByFields tableName howMatched fields = WithConn.Count.byFields tableName howMatched fields conn - /// Count matching documents using a comparison on a JSON field - [] - member conn.countByField tableName field = - conn.countByFields tableName Any [ field ] - /// Determine if a document exists for the given ID member conn.existsById tableName (docId: 'TKey) = WithConn.Exists.byId tableName docId conn @@ -62,11 +57,6 @@ module Extensions = member conn.existsByFields tableName howMatched fields = WithConn.Exists.byFields tableName howMatched fields conn - /// Determine if a document exists using a comparison on a JSON field - [] - member conn.existsByField tableName field = - conn.existsByFields tableName Any [ field ] - /// Retrieve all documents in the given table member conn.findAll<'TDoc> tableName = WithConn.Find.all<'TDoc> tableName conn @@ -87,11 +77,6 @@ module Extensions = member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn - /// Retrieve documents via a comparison on a JSON field - [] - member conn.findByField<'TDoc> tableName field = - conn.findByFields<'TDoc> tableName Any [ field ] - /// Retrieve documents via a comparison on JSON fields, returning only the first result member conn.findFirstByFields<'TDoc> tableName howMatched fields = WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn @@ -101,11 +86,6 @@ module Extensions = member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - member conn.findFirstByField<'TDoc> tableName field = - conn.findFirstByFields<'TDoc> tableName Any [ field ] - /// Update an entire document by its ID member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = WithConn.Update.byId tableName docId document conn @@ -122,11 +102,6 @@ module Extensions = member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = WithConn.Patch.byFields tableName howMatched fields patch conn - /// Patch documents using a comparison on a JSON field - [] - member conn.patchByField tableName field (patch: 'TPatch) = - conn.patchByFields tableName Any [ field ] patch - /// Remove fields from a document by the document's ID member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = WithConn.RemoveFields.byId tableName docId fieldNames conn @@ -135,11 +110,6 @@ module Extensions = member conn.removeFieldsByFields tableName howMatched fields fieldNames = WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn - /// Remove a field from a document via a comparison on a JSON field in the document - [] - member conn.removeFieldsByField tableName field fieldNames = - conn.removeFieldsByFields tableName Any [ field ] fieldNames - /// Delete a document by its ID member conn.deleteById tableName (docId: 'TKey) = WithConn.Delete.byId tableName docId conn @@ -147,11 +117,6 @@ module Extensions = /// Delete documents by matching a comparison on JSON fields member conn.deleteByFields tableName howMatched fields = WithConn.Delete.byFields tableName howMatched fields conn - - /// Delete documents by matching a comparison on a JSON field - [] - member conn.deleteByField tableName field = - conn.deleteByFields tableName Any [ field ] open System.Runtime.CompilerServices @@ -211,12 +176,6 @@ type SqliteConnectionCSharpExtensions = static member inline CountByFields(conn, tableName, howMatched, fields) = WithConn.Count.byFields tableName howMatched fields conn - /// Count matching documents using a comparison on a JSON field - [] - [] - static member inline CountByField(conn, tableName, field) = - conn.CountByFields(tableName, Any, [ field ]) - /// Determine if a document exists for the given ID [] static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) = @@ -227,12 +186,6 @@ type SqliteConnectionCSharpExtensions = static member inline ExistsByFields(conn, tableName, howMatched, fields) = WithConn.Exists.byFields tableName howMatched fields conn - /// Determine if a document exists using a comparison on a JSON field - [] - [] - static member inline ExistsByField(conn, tableName, field) = - conn.ExistsByFields(tableName, Any, [ field ]) - /// Retrieve all documents in the given table [] static member inline FindAll<'TDoc>(conn, tableName) = @@ -241,7 +194,7 @@ type SqliteConnectionCSharpExtensions = /// Retrieve all documents in the given table ordered by the given fields in the document [] static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) = - WithConn.Find.AllOrdered<'TDoc>(tableName, conn, orderFields) + WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn) /// Retrieve a document by its ID [] @@ -258,12 +211,6 @@ type SqliteConnectionCSharpExtensions = static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) = WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) - /// Retrieve documents via a comparison on a JSON field - [] - [] - static member inline FindByField<'TDoc>(conn, tableName, field) = - conn.FindByFields<'TDoc>(tableName, Any, [ field ]) - /// Retrieve documents via a comparison on JSON fields, returning only the first result [] static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = @@ -276,12 +223,6 @@ type SqliteConnectionCSharpExtensions = conn, tableName, howMatched, queryFields, orderFields) = WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ]) - /// Update an entire document by its ID [] static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = @@ -302,12 +243,6 @@ type SqliteConnectionCSharpExtensions = static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) = WithConn.Patch.byFields tableName howMatched fields patch conn - /// Patch documents using a comparison on a JSON field - [] - [] - static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) = - conn.PatchByFields(tableName, Any, [ field ], patch) - /// Remove fields from a document by the document's ID [] static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) = @@ -318,12 +253,6 @@ type SqliteConnectionCSharpExtensions = static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn - /// Remove fields from documents via a comparison on a JSON field in the document - [] - [] - static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames) - /// Delete a document by its ID [] static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) = diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 7fb3afd..accbdb8 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -321,7 +321,7 @@ module WithConn = /// Retrieve all documents in the given table ordered by the given fields in the document let AllOrdered<'TDoc>(tableName, orderFields, conn) = - Custom.List(Query.find tableName + Query.orderBy orderFields PostgreSQL, [], fromData<'TDoc>, conn) + Custom.List(Query.find tableName + Query.orderBy orderFields SQLite, [], fromData<'TDoc>, conn) /// Retrieve a document by its ID (returns None if not found) [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 0dda978..f0d81f9 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -386,43 +386,53 @@ public static class CommonCSharpTests [ TestCase("succeeds for no fields", () => { - Expect.equal("", Query.OrderBy([], Dialect.PostgreSQL), + Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)"); - Expect.equal("", Query.OrderBy([], Dialect.SQLite), "Order By should have been blank (SQLite)"); + Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); }), TestCase("succeeds for PostgreSQL with one field and no direction", () => { - Expect.equal(" ORDER BY data->>'TestField'", - Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), - "Order By not constructed correctly"); + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); }), TestCase("succeeds for SQLite with one field and no direction", () => { - Expect.equal(" ORDER BY data->>'TestField'", - Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), - "Order By not constructed correctly"); + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); }), TestCase("succeeds for PostgreSQL with multiple fields and direction", () => { - Expect.equal(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + Expect.equal( Query.OrderBy( [ Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), Field.Named("It DESC") ], - Dialect.PostgreSQL), "Order By not constructed correctly"); + Dialect.PostgreSQL), + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + "Order By not constructed correctly"); }), TestCase("succeeds for SQLite with multiple fields and direction", () => { Expect.equal( - " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", Query.OrderBy( [ Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), Field.Named("It DESC") ], Dialect.SQLite), + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), + " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); + }), + TestCase("succeeds for SQLite numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", + "Order By not constructed correctly for numeric field"); }) ]) ]) diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 70098ce..7290183 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -1,6 +1,5 @@ using Expecto.CSharp; using Expecto; -using Microsoft.Data.Sqlite; using BitBadger.Documents.Sqlite; namespace BitBadger.Documents.Tests.CSharp; @@ -29,7 +28,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("one") }, Results.FromData); + [Parameters.Id("one")], Results.FromData); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "one", "The incorrect document was returned"); }), @@ -40,7 +39,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("eighty") }, Results.FromData); + [Parameters.Id("eighty")], Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) ]), @@ -63,8 +62,8 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var docs = await conn.CustomList( - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }, Results.FromData); + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", [new("@value", 100)], + Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), @@ -88,7 +87,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }); + [new("@value", 100)]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); @@ -107,38 +106,41 @@ public static class SqliteCSharpExtensionTests await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); - Func> itExists = async name => - await conn.CustomScalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, Results.ToExists); - - var exists = await itExists("ensured"); - var alsoExists = await itExists("idx_ensured_key"); + var exists = await ItExists("ensured"); + var alsoExists = await ItExists("idx_ensured_key"); Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already"); await conn.EnsureTable("ensured"); - exists = await itExists("ensured"); - alsoExists = await itExists("idx_ensured_key"); + exists = await ItExists("ensured"); + alsoExists = await ItExists("idx_ensured_key"); Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + Task ItExists(string name) => + conn.CustomScalar($"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + [new("@name", name)], Results.ToExists); }), TestCase("EnsureFieldIndex succeeds", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); - var indexExists = () => conn.CustomScalar( - $"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 conn.EnsureTable("ensured"); - await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); }), TestList("Insert", [ @@ -213,17 +215,15 @@ public static class SqliteCSharpExtensionTests var theCount = await conn.CountAll(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), -#pragma warning disable CS0618 TestCase("CountByField succeeds", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var theCount = await conn.CountByField(SqliteDb.TableName, Field.EQ("Value", "purple")); + var theCount = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); Expect.equal(theCount, 2L, "There should have been 2 matching documents"); }), -#pragma warning restore CS0618 TestList("ExistsById", [ TestCase("succeeds when a document exists", async () => @@ -245,8 +245,7 @@ public static class SqliteCSharpExtensionTests Expect.isFalse(exists, "There should not have been an existing document"); }) ]), -#pragma warning disable CS0618 - TestList("ExistsByField", + TestList("ExistsByFields", [ TestCase("succeeds when documents exist", async () => { @@ -254,7 +253,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var exists = await conn.ExistsByField(SqliteDb.TableName, Field.GE("NumValue", 10)); + var exists = await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when no matching documents exist", async () => @@ -263,11 +262,11 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var exists = await conn.ExistsByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); + var exists = + await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]); Expect.isFalse(exists, "There should not have been any existing documents"); }) ]), -#pragma warning restore CS0618 TestList("FindAll", [ TestCase("succeeds when there is data", async () => @@ -290,6 +289,43 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(results, "There should have been no documents returned"); }) ]), + TestList("FindAllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), TestList("FindById", [ TestCase("succeeds when a document is found", async () => @@ -312,8 +348,7 @@ public static class SqliteCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning disable CS0618 - TestList("FindByField", + TestList("FindByFields", [ TestCase("succeeds when documents are found", async () => { @@ -321,7 +356,8 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.FindByField(SqliteDb.TableName, Field.GT("NumValue", 15)); + var docs = await conn.FindByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)]); Expect.equal(docs.Count, 2, "There should have been two documents returned"); }), TestCase("succeeds when documents are not found", async () => @@ -330,11 +366,37 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.FindByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); + var docs = await conn.FindByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), - TestList("FindFirstByField", + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id DESC")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "There should have been two documents returned"); + }) + ]), + TestList("FindFirstByFields", [ TestCase("succeeds when a document is found", async () => { @@ -342,7 +404,8 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Value", "another")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned"); }), @@ -352,9 +415,10 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Sub.Foo", "green")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")]); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -362,11 +426,36 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) ]), -#pragma warning restore CS0618 + TestList("FindFirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc!.Id, "An incorrect document was returned"); + }) + ]), TestList("UpdateById", [ TestCase("succeeds when a document is updated", async () => @@ -450,8 +539,7 @@ public static class SqliteCSharpExtensionTests await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" }); }) ]), -#pragma warning disable CS0618 - TestList("PatchByField", + TestList("PatchByFields", [ TestCase("succeeds when a document is updated", async () => { @@ -459,8 +547,9 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await conn.CountByField(SqliteDb.TableName, Field.EQ("NumValue", 77)); + await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -471,10 +560,10 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) ]), -#pragma warning restore CS0618 TestList("RemoveFieldsById", [ TestCase("succeeds when fields are removed", async () => @@ -483,7 +572,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Sub", "Value" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Sub", "Value"]); var updated = await Find.ById(SqliteDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -496,7 +585,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["AFieldThatIsNotThere"]); }), TestCase("succeeds when no document is matched", async () => { @@ -504,11 +593,10 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); // This not raising an exception is the test - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Value" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Value"]); }) ]), -#pragma warning disable CS0618 - TestList("RemoveFieldsByField", + TestList("RemoveFieldsByFields", [ TestCase("succeeds when a field is removed", async () => { @@ -516,7 +604,8 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Sub" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Sub"]); var updated = await Find.ById(SqliteDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNull(updated.Sub, "The sub-document should have been removed"); @@ -528,7 +617,8 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Nothing" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -536,10 +626,10 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); // This not raising an exception is the test - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); }) ]), -#pragma warning restore CS0618 TestList("DeleteById", [ TestCase("succeeds when a document is deleted", async () => @@ -563,8 +653,7 @@ public static class SqliteCSharpExtensionTests Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]), -#pragma warning disable CS0618 - TestList("DeleteByField", + TestList("DeleteByFields", [ TestCase("succeeds when documents are deleted", async () => { @@ -572,7 +661,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.DeleteByField(SqliteDb.TableName, Field.NE("Value", "purple")); + await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); }), @@ -582,12 +671,11 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.DeleteByField(SqliteDb.TableName, Field.EQ("Value", "crimson")); + await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) ]), -#pragma warning restore CS0618 TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) ]); } diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 13cc1f3..b453ff8 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -467,6 +467,39 @@ public static class SqliteCSharpTests Expect.isEmpty(results, "There should have been no documents returned"); }) ]), + TestList("AllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), TestList("ById", [ TestCase("succeeds when a document is found", async () => @@ -508,6 +541,29 @@ public static class SqliteCSharpTests Expect.isEmpty(docs, "There should have been no documents returned"); }) ]), + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id DESC")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "There should have been two documents returned"); + }) + ]), TestList("FirstByFields", [ TestCase("succeeds when a document is found", async () => @@ -539,6 +595,29 @@ public static class SqliteCSharpTests [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) + ]), + TestList("FirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc!.Id, "An incorrect document was returned"); + }) ]) ]); diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 4b3b718..853f4b2 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -277,37 +277,49 @@ let all = } testList "orderBy" [ test "succeeds for no fields" { - Expect.equal "" (Query.orderBy [] PostgreSQL) "Order By should have been blank (PostgreSQL)" - Expect.equal "" (Query.orderBy [] SQLite) "Order By should have been blank (SQLite)" + Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)" + Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)" } test "succeeds for PostgreSQL with one field and no direction" { Expect.equal - " ORDER BY data->>'TestField'" (Query.orderBy [ Field.Named "TestField" ] PostgreSQL) + " ORDER BY data->>'TestField'" "Order By not constructed correctly" } test "succeeds for SQLite with one field and no direction" { Expect.equal - " ORDER BY data->>'TestField'" (Query.orderBy [ Field.Named "TestField" ] SQLite) + " ORDER BY data->>'TestField'" "Order By not constructed correctly" } test "succeeds for PostgreSQL with multiple fields and direction" { Expect.equal - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" (Query.orderBy [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] PostgreSQL) + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" "Order By not constructed correctly" } test "succeeds for SQLite with multiple fields and direction" { Expect.equal - " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" (Query.orderBy [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] SQLite) + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" "Order By not constructed correctly" } + test "succeeds for PostgreSQL numeric fields" { + Expect.equal + (Query.orderBy [ Field.Named "n:Test" ] PostgreSQL) + " ORDER BY (data->>'Test')::numeric" + "Order By not constructed correctly for numeric field" + } + test "succeeds for SQLite numeric fields" { + Expect.equal + (Query.orderBy [ Field.Named "n:Test" ] SQLite) + " ORDER BY data->>'Test'" + "Order By not constructed correctly for numeric field" + } ] ] ] diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index 9fd5bad..51d4f39 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -8,8 +8,6 @@ open Expecto open Microsoft.Data.Sqlite open Types -#nowarn "0044" - /// Integration tests for the F# extensions on the SqliteConnection data type let integrationTests = let loadDocs () = backgroundTask { @@ -115,12 +113,12 @@ let integrationTests = let! theCount = conn.countAll SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } - testTask "countByField succeeds" { + testTask "countByFields succeeds" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! theCount = conn.countByField SqliteDb.TableName (Field.EQ "Value" "purple") + let! theCount = conn.countByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.equal theCount 2L "There should have been 2 matching documents" } testList "existsById" [ @@ -141,13 +139,13 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "existsByField" [ + testList "existsByFields" [ testTask "succeeds when documents exist" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "NumValue" 10) + let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when no matching documents exist" { @@ -155,7 +153,7 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "Nothing" "none") + let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "Nothing" "none" ] Expect.isFalse exists "There should not have been any existing documents" } ] @@ -183,6 +181,44 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] + testList "findAllOrdered" [ + testTask "succeeds when ordering numerically" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "findById" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() @@ -190,7 +226,7 @@ let integrationTests = do! loadDocs () let! doc = conn.findById SqliteDb.TableName "two" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -199,35 +235,59 @@ let integrationTests = do! loadDocs () let! doc = conn.findById SqliteDb.TableName "three hundred eighty-seven" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + Expect.isNone doc "There should not have been a document returned" } ] - testList "findByField" [ + testList "findByFields" [ testTask "succeeds when documents are found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") - Expect.equal (List.length docs) 2 "There should have been two documents returned" + let! docs = conn.findByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.hasLength docs 2 "There should have been two documents returned" } testTask "succeeds when documents are not found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField SqliteDb.TableName (Field.EQ "Value" "mauve") - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + let! docs = conn.findByFields SqliteDb.TableName Any [ Field.EQ "Value" "mauve" ] + Expect.isEmpty docs "There should have been no documents returned" } ] - testList "findFirstByField" [ + testList "findByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.findByFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.findByFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly" + } + ] + testList "findFirstByFields" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Value" "another") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when multiple documents are found" { @@ -235,8 +295,8 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.isSome doc "There should have been a document returned" Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -244,8 +304,32 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Value" "absent") - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "findFirstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.findFirstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.findFirstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" } ] testList "updateById" [ @@ -326,14 +410,14 @@ let integrationTests = do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |} } ] - testList "patchByField" [ + testList "patchByFields" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = conn.countByField SqliteDb.TableName (Field.EQ "NumValue" 77) + do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = conn.countByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] Expect.equal after 2L "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -344,7 +428,7 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] testList "removeFieldsById" [ @@ -377,13 +461,13 @@ let integrationTests = do! conn.removeFieldsById SqliteDb.TableName "two" [ "Value" ] } ] - testList "removeFieldByField" [ + testList "removeFieldByFields" [ testTask "succeeds when a field is removed" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Sub" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] try let! _ = conn.findById SqliteDb.TableName "four" Expect.isTrue false "The updated document should have failed to parse" @@ -397,14 +481,14 @@ let integrationTests = do! loadDocs () // This not raising an exception is the test - do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Nothing" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () // This not raising an exception is the test - do! conn.removeFieldsByField SqliteDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] testList "deleteById" [ @@ -427,13 +511,13 @@ let integrationTests = Expect.equal remaining 5L "There should have been 5 documents remaining" } ] - testList "deleteByField" [ + testList "deleteByFields" [ testTask "succeeds when documents are deleted" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField SqliteDb.TableName (Field.NE "Value" "purple") + do! conn.deleteByFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 2L "There should have been 2 documents remaining" } @@ -442,7 +526,7 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField SqliteDb.TableName (Field.EQ "Value" "crimson") + do! conn.deleteByFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } @@ -480,8 +564,8 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData - Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" + let! docs = conn.customList (Query.find SqliteDb.TableName) [] fromData + Expect.hasLength docs 5 "There should have been 5 documents returned" } testTask "succeeds when data is not found" { use! db = SqliteDb.BuildDb() diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 1006e25..1754e72 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -424,13 +424,48 @@ let findTests = testList "Find" [ Expect.equal results [] "There should have been no documents returned" } ] + testList "allOrdered" [ + testTask "succeeds when ordering numerically" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "byId" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() do! loadDocs () let! doc = Find.byId SqliteDb.TableName "two" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -438,7 +473,7 @@ let findTests = testList "Find" [ do! loadDocs () let! doc = Find.byId SqliteDb.TableName "three hundred eighty-seven" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + Expect.isNone doc "There should not have been a document returned" } ] testList "byFields" [ @@ -457,13 +492,35 @@ let findTests = testList "Find" [ Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" } ] + testList "byFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly" + } + ] testList "firstByFields" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() do! loadDocs () let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when multiple documents are found" { @@ -471,7 +528,7 @@ let findTests = testList "Find" [ do! loadDocs () let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.isSome doc "There should have been a document returned" Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -479,7 +536,29 @@ let findTests = testList "Find" [ do! loadDocs () let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" } ] ] -- 2.45.1 From 88f11a854f8dffb915f04819c010082b7b0b2235 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 17 Aug 2024 14:08:46 -0400 Subject: [PATCH 25/29] Add compat namespace functions - Update package release notes --- src/Directory.Build.props | 2 +- .../BitBadger.Documents.Postgres.fsproj | 1 + src/Postgres/Compat.fs | 270 ++++++++++++++++++ src/Postgres/Library.fs | 12 - src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 1 + src/Sqlite/Compat.fs | 269 +++++++++++++++++ src/Tests.CSharp/PostgresCSharpTests.cs | 23 -- src/Tests/PostgresTests.fs | 14 - 8 files changed, 542 insertions(+), 50 deletions(-) create mode 100644 src/Postgres/Compat.fs create mode 100644 src/Sqlite/Compat.fs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e73f3da..4796c0a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,7 +6,7 @@ 4.0.0.0 4.0.0.0 4.0.0 - Change ByField to ByFields; support dot-access to document fields; add Find*Ordered functions/methods + Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility danieljsummers Bit Badger Solutions README.md diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index 1504c4a..6773b53 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Postgres/Compat.fs b/src/Postgres/Compat.fs new file mode 100644 index 0000000..dae2bfa --- /dev/null +++ b/src/Postgres/Compat.fs @@ -0,0 +1,270 @@ +namespace BitBadger.Documents.Postgres.Compat + +open BitBadger.Documents +open BitBadger.Documents.Postgres + +[] +module Parameters = + + /// Create a JSON field parameter + [] + [] + let addFieldParam name field parameters = + addFieldParams [ { field with ParameterName = Some name } ] parameters + + /// Append JSON field name parameters for the given field names to the given parameters + [] + [] + let fieldNameParam fieldNames = + fieldNameParams fieldNames + + +[] +module Query = + + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document + [] + [] + let whereByField field paramName = + Query.whereByFields Any [ { field with ParameterName = Some paramName } ] + + +module WithProps = + + [] + module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Count.byFields tableName Any [ field ] sqlProps + + [] + module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Exists.byFields tableName Any [ field ] sqlProps + + [] + module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field sqlProps = + WithProps.Find.byFields<'TDoc> tableName Any [ field ] sqlProps + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let ByField<'TDoc>(tableName, field, sqlProps) = + WithProps.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) + + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + [] + let firstByField<'TDoc> tableName field sqlProps = + WithProps.Find.firstByFields<'TDoc> tableName Any [ field ] sqlProps + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = + WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) + + [] + module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) sqlProps = + WithProps.Patch.byFields tableName Any [ field ] patch sqlProps + + [] + module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames sqlProps = + WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames sqlProps + + [] + module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Delete.byFields tableName Any [ field ] sqlProps + + +[] +module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Count.byFields tableName Any [ field ] + + +[] +module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Exists.byFields tableName Any [ field ] + + +[] +module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field = + Find.byFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let ByField<'TDoc>(tableName, field) = + Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field) + + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + [] + let firstByField<'TDoc> tableName field = + Find.firstByFields<'TDoc> tableName Any [ field ] + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) + + +[] +module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) = + Patch.byFields tableName Any [ field ] patch + + +[] +module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames = + RemoveFields.byFields tableName Any [ field ] fieldNames + + +[] +module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field = + Delete.byFields tableName Any [ field ] + + +open Npgsql + +/// F# Extensions for the NpgsqlConnection type +[] +module Extensions = + + type NpgsqlConnection with + + /// Count matching documents using a JSON field comparison query (->> =) + [] + member conn.countByField tableName field = + conn.countByFields tableName Any [ field ] + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + member conn.existsByField tableName field = + conn.existsByFields tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + member conn.findByField<'TDoc> tableName field = + conn.findByFields<'TDoc> tableName Any [ field ] + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] + member conn.findFirstByField<'TDoc> tableName field = + conn.findFirstByFields<'TDoc> tableName Any [ field ] + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + member conn.patchByField tableName field (patch: 'TPatch) = + conn.patchByFields tableName Any [ field ] patch + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + member conn.removeFieldsByField tableName field fieldNames = + conn.removeFieldsByFields tableName Any [ field ] fieldNames + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + member conn.deleteByField tableName field = + conn.deleteByFields tableName Any [ field ] + + +open System.Runtime.CompilerServices +open Npgsql.FSharp + +type NpgsqlConnectionCSharpCompatExtensions = + + /// Count matching documents using a JSON field comparison query (->> =) + [] + [] + static member inline CountByField(conn, tableName, field) = + WithProps.Count.byFields tableName Any [ field ] (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + [] + static member inline ExistsByField(conn, tableName, field) = + WithProps.Exists.byFields tableName Any [ field ] (Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] + static member inline FindByField<'TDoc>(conn, tableName, field) = + WithProps.Find.ByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + [] + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = + WithProps.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn) + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = + WithProps.Patch.byFields tableName Any [ field ] patch (Sql.existingConnection conn) + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = + WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames (Sql.existingConnection conn) + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + static member inline DeleteByField(conn, tableName, field) = + WithProps.Delete.byFields tableName Any [ field ] (Sql.existingConnection conn) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index b5abc7d..54c4d77 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -104,24 +104,12 @@ module Parameters = |> Seq.toList |> Seq.ofList - /// Create a JSON field parameter - [] - [] - let addFieldParam name field parameters = - addFieldParams [ { field with ParameterName = Some name } ] parameters - /// Append JSON field name parameters for the given field names to the given parameters [] let fieldNameParams (fieldNames: string seq) = if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames) else "@name", Sql.stringArray (Array.ofSeq fieldNames) - /// Append JSON field name parameters for the given field names to the given parameters - [] - [] - let fieldNameParam fieldNames = - fieldNameParams fieldNames - /// An empty parameter sequence [] let noParams = diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index 7d46603..f51a328 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Sqlite/Compat.fs b/src/Sqlite/Compat.fs new file mode 100644 index 0000000..7288470 --- /dev/null +++ b/src/Sqlite/Compat.fs @@ -0,0 +1,269 @@ +namespace BitBadger.Documents.Sqlite.Compat + +open BitBadger.Documents +open BitBadger.Documents.Sqlite + +[] +module Parameters = + + /// Create a JSON field parameter + [] + [] + let addFieldParam name field parameters = + addFieldParams [ { field with ParameterName = Some name } ] parameters + + /// Append JSON field name parameters for the given field names to the given parameters + [] + [] + let fieldNameParam fieldNames = + fieldNameParams fieldNames + + +[] +module Query = + + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document + [] + [] + let whereByField field paramName = + Query.whereByFields Any [ { field with ParameterName = Some paramName } ] + + +module WithConn = + + [] + module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field conn = + WithConn.Count.byFields tableName Any [ field ] conn + + [] + module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field conn = + WithConn.Exists.byFields tableName Any [ field ] conn + + [] + module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field conn = + WithConn.Find.byFields<'TDoc> tableName Any [ field ] conn + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let ByField<'TDoc>(tableName, field, conn) = + WithConn.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, conn) + + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + [] + let firstByField<'TDoc> tableName field conn = + WithConn.Find.firstByFields<'TDoc> tableName Any [ field ] conn + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn) + + [] + module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) conn = + WithConn.Patch.byFields tableName Any [ field ] patch conn + + [] + module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames conn = + WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn + + [] + module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field conn = + WithConn.Delete.byFields tableName Any [ field ] conn + + +[] +module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Count.byFields tableName Any [ field ] + + +[] +module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Exists.byFields tableName Any [ field ] + + +[] +module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field = + Find.byFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let ByField<'TDoc>(tableName, field) = + Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field) + + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + [] + let firstByField<'TDoc> tableName field = + Find.firstByFields<'TDoc> tableName Any [ field ] + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) + + +[] +module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) = + Patch.byFields tableName Any [ field ] patch + + +[] +module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames = + RemoveFields.byFields tableName Any [ field ] fieldNames + + +[] +module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field = + Delete.byFields tableName Any [ field ] + + +open Microsoft.Data.Sqlite + +/// F# Extensions for the NpgsqlConnection type +[] +module Extensions = + + type SqliteConnection with + + /// Count matching documents using a JSON field comparison query (->> =) + [] + member conn.countByField tableName field = + conn.countByFields tableName Any [ field ] + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + member conn.existsByField tableName field = + conn.existsByFields tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + member conn.findByField<'TDoc> tableName field = + conn.findByFields<'TDoc> tableName Any [ field ] + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] + member conn.findFirstByField<'TDoc> tableName field = + conn.findFirstByFields<'TDoc> tableName Any [ field ] + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + member conn.patchByField tableName field (patch: 'TPatch) = + conn.patchByFields tableName Any [ field ] patch + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + member conn.removeFieldsByField tableName field fieldNames = + conn.removeFieldsByFields tableName Any [ field ] fieldNames + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + member conn.deleteByField tableName field = + conn.deleteByFields tableName Any [ field ] + + +open System.Runtime.CompilerServices + +type SqliteConnectionCSharpCompatExtensions = + + /// Count matching documents using a JSON field comparison query (->> =) + [] + [] + static member inline CountByField(conn, tableName, field) = + WithConn.Count.byFields tableName Any [ field ] conn + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + [] + static member inline ExistsByField(conn, tableName, field) = + WithConn.Exists.byFields tableName Any [ field ] conn + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] + static member inline FindByField<'TDoc>(conn, tableName, field) = + WithConn.Find.ByFields<'TDoc>(tableName, Any, [ field ], conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + [] + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = + WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn) + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = + WithConn.Patch.byFields tableName Any [ field ] patch conn + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = + WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + static member inline DeleteByField(conn, tableName, field) = + WithConn.Delete.byFields tableName Any [ field ] conn diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 93c3b4d..38324af 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -170,30 +170,7 @@ public static class PostgresCSharpTests Expect.isTrue(false, "The parameter was not a StringArray type"); } }) - ]), -#pragma warning disable CS0618 - TestList("FieldName", - [ - TestCase("succeeds for one name", () => - { - var (name, value) = Parameters.FieldName(["bob"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsString) - { - Expect.isTrue(false, "The parameter was not a String type"); - } - }), - TestCase("succeeds for multiple names", () => - { - var (name, value) = Parameters.FieldName(["bob", "tom", "mike"]); - Expect.equal(name, "@name", "The parameter name was incorrect"); - if (!value.IsStringArray) - { - Expect.isTrue(false, "The parameter was not a StringArray type"); - } - }) ]) -#pragma warning restore CS0618 ]); /// diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 676c8ef..ee31a27 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -5,8 +5,6 @@ open BitBadger.Documents open BitBadger.Documents.Postgres open BitBadger.Documents.Tests -#nowarn "0044" - (** UNIT TESTS **) /// Unit tests for the Parameters module of the PostgreSQL library @@ -129,18 +127,6 @@ let parametersTests = testList "Parameters" [ Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" } ] - testList "fieldNameParam" [ - test "succeeds for one name" { - let name, value = fieldNameParam [ "bob" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.string "bob") "The parameter value was incorrect" - } - test "succeeds for multiple names" { - let name, value = fieldNameParam [ "bob"; "tom"; "mike" ] - Expect.equal name "@name" "The parameter name was incorrect" - Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" - } - ] test "noParams succeeds" { Expect.isEmpty noParams "The no-params sequence should be empty" } -- 2.45.1 From b357b5a01fddc0c8879a0da63cea1806edff1ee5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 18 Aug 2024 17:23:09 -0400 Subject: [PATCH 26/29] Add AutoId DU / tests --- src/Common/Library.fs | 60 ++- src/Tests.CSharp/CommonCSharpTests.cs | 49 ++ src/Tests/CommonTests.fs | 695 ++++++++++++++------------ src/Tests/SqliteTests.fs | 26 - 4 files changed, 495 insertions(+), 335 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 4a9f171..2e1a64d 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -1,5 +1,7 @@ namespace BitBadger.Documents +open System.Security.Cryptography + /// The types of logical operations available for JSON fields [] type Op = @@ -148,6 +150,36 @@ type ParameterName() = currentIdx <- currentIdx + 1 $"@field{currentIdx}" +#if NET6_0 +open System.Text +#endif + +/// Automatically-generated document ID options +[] +type AutoId = + /// No automatic IDs will be generated + | Disabled + /// Generate a MAX-plus-1 numeric value for documents + | Number + /// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string) + | Guid + /// Generate a random string of hexadecimal characters for each document + | RandomString +with + /// Generate a GUID string + static member GenerateGuid() = + System.Guid.NewGuid().ToString "N" + + /// Generate a string of random hexadecimal characters + static member GenerateRandomString(length: int) = +#if NET8_0_OR_GREATER + RandomNumberGenerator.GetHexString(length, lowercase = true) +#else + RandomNumberGenerator.GetBytes((length / 2) + 1) + |> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length) + |> function it -> it.Length <- length; it.ToString() +#endif + /// The required document serialization implementation type IDocumentSerializer = @@ -200,7 +232,7 @@ module Configuration = serializerValue /// The serialized name of the ID field for documents - let mutable idFieldValue = "Id" + let mutable private idFieldValue = "Id" /// Specify the name of the ID field for documents [] @@ -211,6 +243,32 @@ module Configuration = [] let idField () = idFieldValue + + /// The automatic ID strategy used by the library + let mutable private autoIdValue = Disabled + + /// Specify the automatic ID generation strategy used by the library + [] + let useAutoIdStrategy it = + autoIdValue <- it + + /// Retrieve the currently configured automatic ID generation strategy + [] + let autoIdStrategy () = + autoIdValue + + /// The length of automatically generated random strings + let mutable private idStringLengthValue = 16 + + /// Specify the length of automatically generated random strings + [] + let useIdStringLength length = + idStringLengthValue <- length + + /// Retrieve the currently configured length of automatically generated random strings + [] + let idStringLength () = + idStringLengthValue /// Query construction functions diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index f0d81f9..0f07f8f 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -70,6 +70,34 @@ public static class CommonCSharpTests { Configuration.UseIdField("Id"); } + }), + TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () => + { + try + { + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled, + "The default auto-ID strategy was incorrect"); + Configuration.UseAutoIdStrategy(AutoId.Guid); + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid, + "The auto-ID strategy was not set correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("UseIdStringLength / IdStringLength succeeds", () => + { + try + { + Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect"); + Configuration.UseIdStringLength(33); + Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly"); + } + finally + { + Configuration.UseIdStringLength(16); + } }) ])), TestList("Op", @@ -295,6 +323,27 @@ public static class CommonCSharpTests "Counter should have advanced from previous call"); }) ]), + TestList("AutoId", + [ + TestCase("GenerateGuid succeeds", () => + { + var autoId = AutoId.GenerateGuid(); + Expect.isNotNull(autoId, "The GUID auto-ID should not have been null"); + Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase"); + }), + TestCase("GenerateRandomString succeeds", () => + { + foreach (var length in (int[])[6, 8, 12, 20, 32, 57, 64]) + { + var autoId = AutoId.GenerateRandomString(length); + Expect.isNotNull(autoId, $"Random string ({length}) should not have been null"); + Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), + $"Random string ({length}) should have been lowercase"); + } + }) + ]), TestList("Query", [ TestCase("StatementWhere succeeds", () => diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 853f4b2..46b69c9 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -6,320 +6,399 @@ open Expecto /// Test table name let tbl = "test_table" -/// Tests which do not hit the database -let all = - testList "Common" [ - testList "Op" [ - test "EQ succeeds" { - Expect.equal (string EQ) "=" "The equals operator was not correct" - } - test "GT succeeds" { - Expect.equal (string GT) ">" "The greater than operator was not correct" - } - test "GE succeeds" { - Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct" - } - test "LT succeeds" { - Expect.equal (string LT) "<" "The less than operator was not correct" - } - test "LE succeeds" { - Expect.equal (string LE) "<=" "The less than or equal to operator was not correct" - } - test "NE succeeds" { - Expect.equal (string NE) "<>" "The not equal to operator was not correct" - } - test "BT succeeds" { - Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct""" - } - test "EX succeeds" { - Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" - } - test "NEX succeeds" { - Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" - } - ] - testList "Field" [ - test "EQ succeeds" { - let field = Field.EQ "Test" 14 - Expect.equal field.Name "Test" "Field name incorrect" - Expect.equal field.Op EQ "Operator incorrect" - Expect.equal field.Value 14 "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "GT succeeds" { - let field = Field.GT "Great" "night" - Expect.equal field.Name "Great" "Field name incorrect" - Expect.equal field.Op GT "Operator incorrect" - Expect.equal field.Value "night" "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "GE succeeds" { - let field = Field.GE "Nice" 88L - Expect.equal field.Name "Nice" "Field name incorrect" - Expect.equal field.Op GE "Operator incorrect" - Expect.equal field.Value 88L "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "LT succeeds" { - let field = Field.LT "Lesser" "seven" - Expect.equal field.Name "Lesser" "Field name incorrect" - Expect.equal field.Op LT "Operator incorrect" - Expect.equal field.Value "seven" "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "LE succeeds" { - let field = Field.LE "Nobody" "KNOWS"; - Expect.equal field.Name "Nobody" "Field name incorrect" - Expect.equal field.Op LE "Operator incorrect" - Expect.equal field.Value "KNOWS" "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "NE succeeds" { - let field = Field.NE "Park" "here" - Expect.equal field.Name "Park" "Field name incorrect" - Expect.equal field.Op NE "Operator incorrect" - Expect.equal field.Value "here" "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "BT succeeds" { - let field = Field.BT "Age" 18 49 - Expect.equal field.Name "Age" "Field name incorrect" - Expect.equal field.Op BT "Operator incorrect" - Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "EX succeeds" { - let field = Field.EX "Groovy" - Expect.equal field.Name "Groovy" "Field name incorrect" - Expect.equal field.Op EX "Operator incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - test "NEX succeeds" { - let field = Field.NEX "Rad" - Expect.equal field.Name "Rad" "Field name incorrect" - Expect.equal field.Op NEX "Operator incorrect" - Expect.isNone field.ParameterName "The default parameter name should be None" - Expect.isNone field.Qualifier "The default table qualifier should be None" - } - testList "NameToPath" [ - test "succeeds for PostgreSQL and a simple name" { - Expect.equal - "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly" - } - test "succeeds for SQLite and a simple name" { - Expect.equal - "data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly" - } - test "succeeds for PostgreSQL and a nested name" { - Expect.equal - "data#>>'{A,Long,Path,to,the,Property}'" - (Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL) - "Path not constructed correctly" - } - test "succeeds for SQLite and a nested name" { - Expect.equal - "data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'" - (Field.NameToPath "A.Long.Path.to.the.Property" SQLite) - "Path not constructed correctly" - } - ] - test "WithParameterName succeeds" { - let 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" - } - test "WithQualifier succeeds" { - let 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 "Path" [ - test "succeeds for a PostgreSQL single field with no qualifier" { - let field = Field.GE "SomethingCool" 18 - Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" - } - test "succeeds for a PostgreSQL single field with a qualifier" { - let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } - Expect.equal - "this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" - } - test "succeeds for a PostgreSQL nested field with no qualifier" { - let field = Field.EQ "My.Nested.Field" "howdy" - Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" - } - test "succeeds for a PostgreSQL nested field with a qualifier" { - let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } - Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" - } - test "succeeds for a SQLite single field with no qualifier" { - let field = Field.GE "SomethingCool" 18 - Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect" - } - test "succeeds for a SQLite single field with a qualifier" { - let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } - Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect" - } - test "succeeds for a SQLite nested field with no qualifier" { - let field = Field.EQ "My.Nested.Field" "howdy" - Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect" - } - test "succeeds for a SQLite nested field with a qualifier" { - let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } - Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect" - } - ] - ] - testList "FieldMatch.ToString" [ - test "succeeds for Any" { - Expect.equal (string Any) "OR" "SQL for Any is incorrect" - } - test "succeeds for All" { - Expect.equal (string All) "AND" "SQL for All is incorrect" - } - ] - testList "ParameterName.Derive" [ - test "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 "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 "statementWhere succeeds" { - Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly" - } - testList "Definition" [ - test "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" [ - test "succeeds when a schema is present" { - Expect.equal - (Query.Definition.ensureKey "test.table" PostgreSQL) - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))" - "CREATE INDEX for key statement with schema not constructed correctly" - } - test "succeeds when a schema is not present" { - Expect.equal - (Query.Definition.ensureKey "table" SQLite) - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))" - "CREATE INDEX for key statement without schema not constructed correctly" - } - ] - testList "ensureIndexOn" [ - test "succeeds for multiple fields and directions" { - Expect.equal - (Query.Definition.ensureIndexOn - "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) - ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] - |> String.concat "") - "CREATE INDEX for multiple field statement incorrect" - } - test "succeeds for nested PostgreSQL field" { - Expect.equal - (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL) - $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))" - "CREATE INDEX for nested PostgreSQL field incorrect" - } - test "succeeds for nested SQLite field" { - Expect.equal - (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite) - $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))" - "CREATE INDEX for nested SQLite field incorrect" - } - ] - ] - test "insert succeeds" { - Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" - } - test "save succeeds" { +/// Unit tests for the Op DU +let opTests = testList "Op" [ + test "EQ succeeds" { + Expect.equal (string EQ) "=" "The equals operator was not correct" + } + test "GT succeeds" { + Expect.equal (string GT) ">" "The greater than operator was not correct" + } + test "GE succeeds" { + Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct" + } + test "LT succeeds" { + Expect.equal (string LT) "<" "The less than operator was not correct" + } + test "LE succeeds" { + Expect.equal (string LE) "<=" "The less than or equal to operator was not correct" + } + test "NE succeeds" { + Expect.equal (string NE) "<>" "The not equal to operator was not correct" + } + test "BT succeeds" { + Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct""" + } + test "EX succeeds" { + Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" + } + test "NEX succeeds" { + Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" + } +] + +/// Unit tests for the Field class +let fieldTests = testList "Field" [ + test "EQ succeeds" { + let field = Field.EQ "Test" 14 + Expect.equal field.Name "Test" "Field name incorrect" + Expect.equal field.Op EQ "Operator incorrect" + Expect.equal field.Value 14 "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "GT succeeds" { + let field = Field.GT "Great" "night" + Expect.equal field.Name "Great" "Field name incorrect" + Expect.equal field.Op GT "Operator incorrect" + Expect.equal field.Value "night" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "GE succeeds" { + let field = Field.GE "Nice" 88L + Expect.equal field.Name "Nice" "Field name incorrect" + Expect.equal field.Op GE "Operator incorrect" + Expect.equal field.Value 88L "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "LT succeeds" { + let field = Field.LT "Lesser" "seven" + Expect.equal field.Name "Lesser" "Field name incorrect" + Expect.equal field.Op LT "Operator incorrect" + Expect.equal field.Value "seven" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "LE succeeds" { + let field = Field.LE "Nobody" "KNOWS"; + Expect.equal field.Name "Nobody" "Field name incorrect" + Expect.equal field.Op LE "Operator incorrect" + Expect.equal field.Value "KNOWS" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "NE succeeds" { + let field = Field.NE "Park" "here" + Expect.equal field.Name "Park" "Field name incorrect" + Expect.equal field.Op NE "Operator incorrect" + Expect.equal field.Value "here" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "BT succeeds" { + let field = Field.BT "Age" 18 49 + Expect.equal field.Name "Age" "Field name incorrect" + Expect.equal field.Op BT "Operator incorrect" + Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "EX succeeds" { + let field = Field.EX "Groovy" + Expect.equal field.Name "Groovy" "Field name incorrect" + Expect.equal field.Op EX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "NEX succeeds" { + let field = Field.NEX "Rad" + Expect.equal field.Name "Rad" "Field name incorrect" + Expect.equal field.Op NEX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + testList "NameToPath" [ + test "succeeds for PostgreSQL and a simple name" { + Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly" + } + test "succeeds for SQLite and a simple name" { + Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly" + } + test "succeeds for PostgreSQL and a nested name" { + Expect.equal + "data#>>'{A,Long,Path,to,the,Property}'" + (Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL) + "Path not constructed correctly" + } + test "succeeds for SQLite and a nested name" { + Expect.equal + "data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'" + (Field.NameToPath "A.Long.Path.to.the.Property" SQLite) + "Path not constructed correctly" + } + ] + test "WithParameterName succeeds" { + let 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" + } + test "WithQualifier succeeds" { + let 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 "Path" [ + test "succeeds for a PostgreSQL single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a SQLite single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect" + } + ] +] + +/// Unit tests for the FieldMatch DU +let fieldMatchTests = testList "FieldMatch.ToString" [ + test "succeeds for Any" { + Expect.equal (string Any) "OR" "SQL for Any is incorrect" + } + test "succeeds for All" { + Expect.equal (string All) "AND" "SQL for All is incorrect" + } +] + +/// Unit tests for the ParameterName class +let parameterNameTests = testList "ParameterName.Derive" [ + test "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 "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" + } +] + +/// Unit tests for the AutoId DU +let autoIdTests = testList "AutoId" [ + test "GenerateGuid succeeds" { + let autoId = AutoId.GenerateGuid() + Expect.isNotNull autoId "The GUID auto-ID should not have been null" + Expect.stringHasLength autoId 32 "The GUID auto-ID should have been 32 characters long" + Expect.equal autoId (autoId.ToLowerInvariant ()) "The GUID auto-ID should have been lowercase" + } + test "GenerateRandomString succeeds" { + [ 6; 8; 12; 20; 32; 57; 64 ] + |> List.iter (fun length -> + let autoId = AutoId.GenerateRandomString length + Expect.isNotNull autoId $"Random string ({length}) should not have been null" + Expect.stringHasLength autoId length $"Random string should have been {length} characters long" + Expect.equal autoId (autoId.ToLowerInvariant ()) $"Random string ({length}) should have been lowercase") + } +] + +/// Unit tests for the Query module +let queryTests = testList "Query" [ + test "statementWhere succeeds" { + Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly" + } + testList "Definition" [ + test "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" [ + test "succeeds when a schema is present" { Expect.equal - (Query.save tbl) - $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" - "INSERT ON CONFLICT UPDATE statement not correct" + (Query.Definition.ensureKey "test.table" PostgreSQL) + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))" + "CREATE INDEX for key statement with schema not constructed correctly" } - test "count succeeds" { - Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct" - } - test "exists succeeds" { + test "succeeds when a schema is not present" { Expect.equal - (Query.exists tbl "turkey") - $"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it" - "Exists query not correct" + (Query.Definition.ensureKey "table" SQLite) + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))" + "CREATE INDEX for key statement without schema not constructed correctly" } - test "find succeeds" { - Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct" + ] + testList "ensureIndexOn" [ + test "succeeds for multiple fields and directions" { + Expect.equal + (Query.Definition.ensureIndexOn + "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) + ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] + |> String.concat "") + "CREATE INDEX for multiple field statement incorrect" } - test "update succeeds" { - Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct" + test "succeeds for nested PostgreSQL field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))" + "CREATE INDEX for nested PostgreSQL field incorrect" } - test "delete succeeds" { - Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct" + test "succeeds for nested SQLite field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))" + "CREATE INDEX for nested SQLite field incorrect" } - testList "orderBy" [ - test "succeeds for no fields" { - Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)" - Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)" - } - test "succeeds for PostgreSQL with one field and no direction" { - Expect.equal - (Query.orderBy [ Field.Named "TestField" ] PostgreSQL) - " ORDER BY data->>'TestField'" - "Order By not constructed correctly" - } - test "succeeds for SQLite with one field and no direction" { - Expect.equal - (Query.orderBy [ Field.Named "TestField" ] SQLite) - " ORDER BY data->>'TestField'" - "Order By not constructed correctly" - } - test "succeeds for PostgreSQL with multiple fields and direction" { - Expect.equal - (Query.orderBy - [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] - PostgreSQL) - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" - "Order By not constructed correctly" - } - test "succeeds for SQLite with multiple fields and direction" { - Expect.equal - (Query.orderBy - [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] - SQLite) - " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" - "Order By not constructed correctly" - } - test "succeeds for PostgreSQL numeric fields" { - Expect.equal - (Query.orderBy [ Field.Named "n:Test" ] PostgreSQL) - " ORDER BY (data->>'Test')::numeric" - "Order By not constructed correctly for numeric field" - } - test "succeeds for SQLite numeric fields" { - Expect.equal - (Query.orderBy [ Field.Named "n:Test" ] SQLite) - " ORDER BY data->>'Test'" - "Order By not constructed correctly for numeric field" - } - ] ] ] + test "insert succeeds" { + Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" + } + test "save succeeds" { + Expect.equal + (Query.save tbl) + $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" + "INSERT ON CONFLICT UPDATE statement not correct" + } + test "count succeeds" { + Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct" + } + test "exists succeeds" { + Expect.equal + (Query.exists tbl "turkey") + $"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it" + "Exists query not correct" + } + test "find succeeds" { + Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct" + } + test "update succeeds" { + Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct" + } + test "delete succeeds" { + Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct" + } + testList "orderBy" [ + test "succeeds for no fields" { + Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)" + Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)" + } + test "succeeds for PostgreSQL with one field and no direction" { + Expect.equal + (Query.orderBy [ Field.Named "TestField" ] PostgreSQL) + " ORDER BY data->>'TestField'" + "Order By not constructed correctly" + } + test "succeeds for SQLite with one field and no direction" { + Expect.equal + (Query.orderBy [ Field.Named "TestField" ] SQLite) + " ORDER BY data->>'TestField'" + "Order By not constructed correctly" + } + test "succeeds for PostgreSQL with multiple fields and direction" { + Expect.equal + (Query.orderBy + [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] + PostgreSQL) + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" + "Order By not constructed correctly" + } + test "succeeds for SQLite with multiple fields and direction" { + Expect.equal + (Query.orderBy + [ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ] + SQLite) + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" + "Order By not constructed correctly" + } + test "succeeds for PostgreSQL numeric fields" { + Expect.equal + (Query.orderBy [ Field.Named "n:Test" ] PostgreSQL) + " ORDER BY (data->>'Test')::numeric" + "Order By not constructed correctly for numeric field" + } + test "succeeds for SQLite numeric fields" { + Expect.equal + (Query.orderBy [ Field.Named "n:Test" ] SQLite) + " ORDER BY data->>'Test'" + "Order By not constructed correctly for numeric field" + } + ] +] + +/// Unit tests for the Configuration module +let configurationTests = testList "Configuration" [ + test "useSerializer succeeds" { + try + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" + member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> + } + + let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} + Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" + + let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" + Expect.isNull deserialized "Specified serializer should have returned null" + finally + Configuration.useSerializer DocumentSerializer.``default`` + } + test "serializer returns configured serializer" { + Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) + "Serializer should have been the same" + } + test "useIdField / idField succeeds" { + try + Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" + Configuration.useIdField "id" + Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" + finally + Configuration.useIdField "Id" + } + test "useAutoIdStrategy / autoIdStrategy succeeds" { + try + Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect" + Configuration.useAutoIdStrategy Guid + Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly" + finally + Configuration.useAutoIdStrategy Disabled + } + test "useIdStringLength / idStringLength succeeds" { + try + Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect" + Configuration.useIdStringLength 33 + Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly" + finally + Configuration.useIdStringLength 16 + } +] + +/// Tests which do not hit the database +let all = testList "Common" [ + opTests + fieldTests + fieldMatchTests + parameterNameTests + autoIdTests + queryTests + testSequenced configurationTests +] diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 1754e72..16b91eb 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -144,32 +144,6 @@ let configurationTests = testList "Configuration" [ finally Configuration.useConnectionString "Data Source=:memory:" } - test "useSerializer succeeds" { - try - Configuration.useSerializer - { new IDocumentSerializer with - member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" - member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> - } - - let serialized = Configuration.serializer().Serialize { Foo = "howdy"; Bar = "bye"} - Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" - - let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" - Expect.isNull deserialized "Specified serializer should have returned null" - finally - Configuration.useSerializer DocumentSerializer.``default`` - } - test "serializer returns configured serializer" { - Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) - "Serializer should have been the same" - } - test "useIdField / idField succeeds" { - Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" - Configuration.useIdField "id" - Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" - Configuration.useIdField "Id" - } ] /// Integration tests for the Custom module of the SQLite library -- 2.45.1 From f41fa1245a8a60a7d8d0949b1d4618baa972dc33 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 18 Aug 2024 21:57:58 -0400 Subject: [PATCH 27/29] Implement auto IDs for both dbs - Move SQLite WithConn doc into Document module --- src/Common/Library.fs | 40 +- src/Postgres/Library.fs | 18 +- src/Sqlite/Extensions.fs | 8 +- src/Sqlite/Library.fs | 41 +- src/Tests.CSharp/CommonCSharpTests.cs | 1061 +++++++++++--------- src/Tests.CSharp/PostgresCSharpTests.cs | 92 +- src/Tests.CSharp/SqliteCSharpTests.cs | 79 +- src/Tests.CSharp/Types.cs | 6 + src/Tests/BitBadger.Documents.Tests.fsproj | 2 +- src/Tests/CommonTests.fs | 157 ++- src/Tests/PostgresTests.fs | 73 +- src/Tests/SqliteTests.fs | 73 +- src/Tests/Types.fs | 4 + 13 files changed, 1097 insertions(+), 557 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 2e1a64d..428c859 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -154,7 +154,7 @@ type ParameterName() = open System.Text #endif -/// Automatically-generated document ID options +/// Automatically-generated document ID strategies [] type AutoId = /// No automatic IDs will be generated @@ -167,11 +167,11 @@ type AutoId = | RandomString with /// Generate a GUID string - static member GenerateGuid() = + static member GenerateGuid () = System.Guid.NewGuid().ToString "N" /// Generate a string of random hexadecimal characters - static member GenerateRandomString(length: int) = + static member GenerateRandomString (length: int) = #if NET8_0_OR_GREATER RandomNumberGenerator.GetHexString(length, lowercase = true) #else @@ -179,6 +179,40 @@ with |> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length) |> function it -> it.Length <- length; it.ToString() #endif + + /// Does the given document need an automatic ID generated? + static member NeedsAutoId<'T> strategy (document: 'T) idProp = + match strategy with + | Disabled -> false + | _ -> + let prop = document.GetType().GetProperty idProp + if isNull prop then invalidOp $"{idProp} not found in document" + else + match strategy with + | Number -> + if prop.PropertyType = typeof then + let value = prop.GetValue document :?> int8 + value = int8 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int16 + value = int16 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int + value = 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int64 + value = int64 0 + else invalidOp "Document ID was not a number; cannot auto-generate a Number ID" + | Guid | RandomString -> + if prop.PropertyType = typeof then + let value = + prop.GetValue document + |> Option.ofObj + |> Option.map (fun it -> it :?> string) + |> Option.defaultValue "" + value = "" + else invalidOp "Document ID was not a string; cannot auto-generate GUID or random string" + | Disabled -> false /// The required document serialization implementation diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 54c4d77..4095549 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -312,7 +312,23 @@ module WithProps = /// Insert a new document [] let insert<'TDoc> tableName (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps + let query = + match Configuration.autoIdStrategy () with + | Disabled -> Query.insert tableName + | strategy -> + let idField = Configuration.idField () + let dataParam = + if AutoId.NeedsAutoId strategy document idField then + match strategy with + | Number -> + $"' || (SELECT COALESCE(MAX((data->>'{idField}')::numeric), 0) + 1 FROM {tableName}) || '" + | Guid -> $"\"{AutoId.GenerateGuid()}\"" + | RandomString -> $"\"{AutoId.GenerateRandomString(Configuration.idStringLength ())}\"" + | Disabled -> "@data" + |> function it -> $"""@data::jsonb || ('{{"{idField}":{it}}}')::jsonb""" + else "@data" + (Query.insert tableName).Replace("@data", dataParam) + Custom.nonQuery query [ jsonParam "@data" document ] sqlProps /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index d8b5106..4a32dec 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -35,11 +35,11 @@ module Extensions = /// Insert a new document member conn.insert<'TDoc> tableName (document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") member conn.save<'TDoc> tableName (document: 'TDoc) = - WithConn.save tableName document conn + WithConn.Document.save tableName document conn /// Count all documents in a table member conn.countAll tableName = @@ -159,12 +159,12 @@ type SqliteConnectionCSharpExtensions = /// Insert a new document [] static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.save<'TDoc> tableName document conn + WithConn.Document.save<'TDoc> tableName document conn /// Count all documents in a table [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index accbdb8..d34d3f1 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -258,15 +258,34 @@ module WithConn = let ensureFieldIndex tableName indexName fields conn = Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn - /// Insert a new document - [] - let insert<'TDoc> tableName (document: 'TDoc) conn = - Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] conn - - /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - [] - let save<'TDoc> tableName (document: 'TDoc) conn = - Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn + /// Commands to add documents + [] + module Document = + + /// Insert a new document + [] + let insert<'TDoc> tableName (document: 'TDoc) conn = + let query = + match Configuration.autoIdStrategy () with + | Disabled -> Query.insert tableName + | strategy -> + let idField = Configuration.idField () + let dataParam = + if AutoId.NeedsAutoId strategy document idField then + match strategy with + | Number -> $"(SELECT coalesce(max(data->>'{idField}'), 0) + 1 FROM {tableName})" + | Guid -> $"'{AutoId.GenerateGuid()}'" + | RandomString -> $"'{AutoId.GenerateRandomString(Configuration.idStringLength ())}'" + | Disabled -> "@data" + |> function it -> $"json_set(@data, '$.{idField}', {it})" + else "@data" + (Query.insert tableName).Replace("@data", dataParam) + Custom.nonQuery query [ jsonParam "@data" document ] conn + + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save<'TDoc> tableName (document: 'TDoc) conn = + Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn /// Commands to count documents [] @@ -547,13 +566,13 @@ module Document = [] let insert<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.insert tableName document conn + WithConn.Document.insert tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] let save<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.save tableName document conn + WithConn.Document.save tableName document conn /// Commands to count documents diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 0f07f8f..0a0ca67 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -21,469 +21,614 @@ internal class TestSerializer : IDocumentSerializer /// public static class CommonCSharpTests { + /// + /// Unit tests for the Op enum + /// + private static readonly Test OpTests = TestList("Op", + [ + TestCase("EQ succeeds", () => + { + Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); + }), + TestCase("GT succeeds", () => + { + Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); + }), + TestCase("GE succeeds", () => + { + Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); + }), + TestCase("LT succeeds", () => + { + Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); + }), + TestCase("LE succeeds", () => + { + Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); + }), + TestCase("NE succeeds", () => + { + Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); + }), + TestCase("BT succeeds", () => + { + Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct"); + }), + TestCase("EX succeeds", () => + { + Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); + }), + TestCase("NEX succeeds", () => + { + Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); + }) + ]); + + /// + /// Unit tests for the Field class + /// + private static readonly Test FieldTests = TestList("Field", + [ + TestCase("EQ succeeds", () => + { + var field = Field.EQ("Test", 14); + Expect.equal(field.Name, "Test", "Field name incorrect"); + Expect.equal(field.Op, Op.EQ, "Operator incorrect"); + Expect.equal(field.Value, 14, "Value incorrect"); + }), + TestCase("GT succeeds", () => + { + var field = Field.GT("Great", "night"); + Expect.equal(field.Name, "Great", "Field name incorrect"); + Expect.equal(field.Op, Op.GT, "Operator incorrect"); + Expect.equal(field.Value, "night", "Value incorrect"); + }), + TestCase("GE succeeds", () => + { + var field = Field.GE("Nice", 88L); + Expect.equal(field.Name, "Nice", "Field name incorrect"); + Expect.equal(field.Op, Op.GE, "Operator incorrect"); + Expect.equal(field.Value, 88L, "Value incorrect"); + }), + TestCase("LT succeeds", () => + { + var field = Field.LT("Lesser", "seven"); + Expect.equal(field.Name, "Lesser", "Field name incorrect"); + Expect.equal(field.Op, Op.LT, "Operator incorrect"); + Expect.equal(field.Value, "seven", "Value incorrect"); + }), + TestCase("LE succeeds", () => + { + var field = Field.LE("Nobody", "KNOWS"); + Expect.equal(field.Name, "Nobody", "Field name incorrect"); + Expect.equal(field.Op, Op.LE, "Operator incorrect"); + Expect.equal(field.Value, "KNOWS", "Value incorrect"); + }), + TestCase("NE succeeds", () => + { + var field = Field.NE("Park", "here"); + Expect.equal(field.Name, "Park", "Field name incorrect"); + Expect.equal(field.Op, Op.NE, "Operator incorrect"); + Expect.equal(field.Value, "here", "Value incorrect"); + }), + TestCase("BT succeeds", () => + { + 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(), [18, 49], "Value incorrect"); + }), + TestCase("EX succeeds", () => + { + var field = Field.EX("Groovy"); + Expect.equal(field.Name, "Groovy", "Field name incorrect"); + Expect.equal(field.Op, Op.EX, "Operator incorrect"); + }), + TestCase("NEX succeeds", () => + { + var field = Field.NEX("Rad"); + Expect.equal(field.Name, "Rad", "Field name incorrect"); + Expect.equal(field.Op, Op.NEX, "Operator incorrect"); + }), + TestList("NameToPath", + [ + TestCase("succeeds for PostgreSQL and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), + "Path not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL and a nested name", () => + { + Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a nested name", () => + { + Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), + "Path not constructed correctly"); + }) + ]), + 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("Path", + [ + TestCase("succeeds for a PostgreSQL single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }) + ]) + ]); + + /// + /// Unit tests for the FieldMatch enum + /// + private static readonly Test FieldMatchTests = TestList("FieldMatch.ToString", + [ + TestCase("succeeds for Any", () => + { + Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); + }), + TestCase("succeeds for All", () => + { + Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); + }) + ]); + + /// + /// Unit tests for the ParameterName class + /// + private static readonly Test ParameterNameTests = TestList("ParameterName.Derive", + [ + TestCase("succeeds with existing name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Counter should not have advanced for named field"); + }), + TestCase("Derive succeeds with non-existent name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Anonymous field name should have been returned"); + Expect.equal(name.Derive(FSharpOption.None), "@field1", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field2", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field3", + "Counter should have advanced from previous call"); + }) + ]); + + /// + /// Unit tests for the AutoId enum + /// + private static readonly Test AutoIdTests = TestList("AutoId", + [ + TestCase("GenerateGuid succeeds", () => + { + var autoId = AutoId.GenerateGuid(); + Expect.isNotNull(autoId, "The GUID auto-ID should not have been null"); + Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase"); + }), + TestCase("GenerateRandomString succeeds", () => + { + foreach (var length in (int[]) [6, 8, 12, 20, 32, 57, 64]) + { + var autoId = AutoId.GenerateRandomString(length); + Expect.isNotNull(autoId, $"Random string ({length}) should not have been null"); + Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), + $"Random string ({length}) should have been lowercase"); + } + }), + TestList("NeedsAutoId", + [ + TestCase("succeeds when no auto ID is configured", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Disabled, new object(), "id"), + "Disabled auto-ID never needs an automatic ID"); + }), + TestCase("fails for any when the ID property is not found", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Number, new { Key = "" }, "Id"); + Expect.isTrue(false, "Non-existent ID property should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for byte when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for byte when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)4 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for short when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for short when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)7 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for int when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for int when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 32 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for long when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0L }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for long when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 80L }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("fails for number when the ID is not a number", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Number, new { Id = "" }, "Id"); + Expect.isTrue(false, "Numeric ID against a string should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for GUID when the ID is blank", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "" }, "Id"), + "Blank ID should have returned true"); + }), + TestCase("succeeds for GUID when the ID is filled", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "abc" }, "Id"), + "Filled ID should have returned false"); + }), + TestCase("fails for GUID when the ID is not a string", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Guid, new { Id = 8 }, "Id"); + Expect.isTrue(false, "String ID against a number should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for RandomString when the ID is blank", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "" }, "Id"), + "Blank ID should have returned true"); + }), + TestCase("succeeds for RandomString when the ID is filled", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "x" }, "Id"), + "Filled ID should have returned false"); + }), + TestCase("fails for RandomString when the ID is not a string", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.RandomString, new { Id = 33 }, "Id"); + Expect.isTrue(false, "String ID against a number should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }) + ]) + ]); + + /// + /// Unit tests for the Configuration static class + /// + private static readonly Test ConfigurationTests = TestList("Configuration", + [ + TestCase("UseSerializer succeeds", () => + { + try + { + Configuration.UseSerializer(new TestSerializer()); + + var serialized = Configuration.Serializer().Serialize(new SubDocument + { + Foo = "howdy", + Bar = "bye" + }); + Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); + + var deserialized = Configuration.Serializer() + .Deserialize("{\"Something\":\"here\"}"); + Expect.isNull(deserialized, "Specified serializer should have returned null"); + } + finally + { + Configuration.UseSerializer(DocumentSerializer.Default); + } + }), + TestCase("Serializer returns configured serializer", () => + { + Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), + "Serializer should have been the same"); + }), + TestCase("UseIdField / IdField succeeds", () => + { + try + { + Expect.equal(Configuration.IdField(), "Id", + "The default configured ID field was incorrect"); + Configuration.UseIdField("id"); + Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); + } + finally + { + Configuration.UseIdField("Id"); + } + }), + TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () => + { + try + { + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled, + "The default auto-ID strategy was incorrect"); + Configuration.UseAutoIdStrategy(AutoId.Guid); + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid, + "The auto-ID strategy was not set correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("UseIdStringLength / IdStringLength succeeds", () => + { + try + { + Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect"); + Configuration.UseIdStringLength(33); + Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly"); + } + finally + { + Configuration.UseIdStringLength(16); + } + }) + ]); + + /// + /// Unit tests for the Query static class + /// + private static readonly Test QueryTests = TestList("Query", + [ + TestCase("StatementWhere succeeds", () => + { + Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); + }), + 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", + [ + TestCase("succeeds when a schema is present", () => + { + Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", + "CREATE INDEX for key statement with schema not constructed correctly"); + }), + TestCase("succeeds when a schema is not present", () => + { + Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", + "CREATE INDEX for key statement without schema not constructed correctly"); + }) + ]), + TestList("EnsureIndexOn", + [ + TestCase("succeeds for multiple fields and directions", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("test.table", "gibberish", + ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + "CREATE INDEX for multiple field statement incorrect"); + }), + TestCase("succeeds for nested PostgreSQL field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", + "CREATE INDEX for nested PostgreSQL field incorrect"); + }), + TestCase("succeeds for nested SQLite field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", + "CREATE INDEX for nested SQLite field incorrect"); + }) + ]) + ]), + TestCase("Insert succeeds", () => + { + Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + }), + TestCase("Save succeeds", () => + { + Expect.equal(Query.Save("tbl"), + "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", + "INSERT ON CONFLICT UPDATE statement not correct"); + }), + TestCase("Count succeeds", () => + { + Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); + }), + TestCase("Exists succeeds", () => + { + Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", + "Exists query not correct"); + }), + TestCase("Find succeeds", () => + { + Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); + }), + TestCase("Update succeeds", () => + { + Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); + }), + TestCase("Delete succeeds", () => + { + Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); + }), + TestList("OrderBy", + [ + TestCase("succeeds for no fields", () => + { + Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)"); + Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); + }), + TestCase("succeeds for PostgreSQL with one field and no direction", () => + { + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with one field and no direction", () => + { + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL with multiple fields and direction", () => + { + Expect.equal( + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], Dialect.PostgreSQL), + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with multiple fields and direction", () => + { + Expect.equal( + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], Dialect.SQLite), + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), + " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); + }), + TestCase("succeeds for SQLite numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", + "Order By not constructed correctly for numeric field"); + }) + ]) + ]); + /// /// Unit tests /// [Tests] public static readonly Test Unit = TestList("Common.C# Unit", [ - TestSequenced( - TestList("Configuration", - [ - TestCase("UseSerializer succeeds", () => - { - try - { - Configuration.UseSerializer(new TestSerializer()); - - var serialized = Configuration.Serializer().Serialize(new SubDocument - { - Foo = "howdy", - Bar = "bye" - }); - Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); - - var deserialized = Configuration.Serializer() - .Deserialize("{\"Something\":\"here\"}"); - Expect.isNull(deserialized, "Specified serializer should have returned null"); - } - finally - { - Configuration.UseSerializer(DocumentSerializer.Default); - } - }), - TestCase("Serializer returns configured serializer", () => - { - Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), - "Serializer should have been the same"); - }), - TestCase("UseIdField / IdField succeeds", () => - { - try - { - Expect.equal(Configuration.IdField(), "Id", - "The default configured ID field was incorrect"); - Configuration.UseIdField("id"); - Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); - } - finally - { - Configuration.UseIdField("Id"); - } - }), - TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () => - { - try - { - Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled, - "The default auto-ID strategy was incorrect"); - Configuration.UseAutoIdStrategy(AutoId.Guid); - Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid, - "The auto-ID strategy was not set correctly"); - } - finally - { - Configuration.UseAutoIdStrategy(AutoId.Disabled); - } - }), - TestCase("UseIdStringLength / IdStringLength succeeds", () => - { - try - { - Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect"); - Configuration.UseIdStringLength(33); - Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly"); - } - finally - { - Configuration.UseIdStringLength(16); - } - }) - ])), - TestList("Op", - [ - TestCase("EQ succeeds", () => - { - Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); - }), - TestCase("GT succeeds", () => - { - Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); - }), - TestCase("GE succeeds", () => - { - Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); - }), - TestCase("LT succeeds", () => - { - Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); - }), - TestCase("LE succeeds", () => - { - Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); - }), - TestCase("NE succeeds", () => - { - Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); - }), - TestCase("BT succeeds", () => - { - Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct"); - }), - TestCase("EX succeeds", () => - { - Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); - }), - TestCase("NEX succeeds", () => - { - Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); - }) - ]), - TestList("Field", - [ - TestCase("EQ succeeds", () => - { - var field = Field.EQ("Test", 14); - Expect.equal(field.Name, "Test", "Field name incorrect"); - Expect.equal(field.Op, Op.EQ, "Operator incorrect"); - Expect.equal(field.Value, 14, "Value incorrect"); - }), - TestCase("GT succeeds", () => - { - var field = Field.GT("Great", "night"); - Expect.equal(field.Name, "Great", "Field name incorrect"); - Expect.equal(field.Op, Op.GT, "Operator incorrect"); - Expect.equal(field.Value, "night", "Value incorrect"); - }), - TestCase("GE succeeds", () => - { - var field = Field.GE("Nice", 88L); - Expect.equal(field.Name, "Nice", "Field name incorrect"); - Expect.equal(field.Op, Op.GE, "Operator incorrect"); - Expect.equal(field.Value, 88L, "Value incorrect"); - }), - TestCase("LT succeeds", () => - { - var field = Field.LT("Lesser", "seven"); - Expect.equal(field.Name, "Lesser", "Field name incorrect"); - Expect.equal(field.Op, Op.LT, "Operator incorrect"); - Expect.equal(field.Value, "seven", "Value incorrect"); - }), - TestCase("LE succeeds", () => - { - var field = Field.LE("Nobody", "KNOWS"); - Expect.equal(field.Name, "Nobody", "Field name incorrect"); - Expect.equal(field.Op, Op.LE, "Operator incorrect"); - Expect.equal(field.Value, "KNOWS", "Value incorrect"); - }), - TestCase("NE succeeds", () => - { - var field = Field.NE("Park", "here"); - Expect.equal(field.Name, "Park", "Field name incorrect"); - Expect.equal(field.Op, Op.NE, "Operator incorrect"); - Expect.equal(field.Value, "here", "Value incorrect"); - }), - TestCase("BT succeeds", () => - { - 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(), [18, 49], "Value incorrect"); - }), - TestCase("EX succeeds", () => - { - var field = Field.EX("Groovy"); - Expect.equal(field.Name, "Groovy", "Field name incorrect"); - Expect.equal(field.Op, Op.EX, "Operator incorrect"); - }), - TestCase("NEX succeeds", () => - { - var field = Field.NEX("Rad"); - Expect.equal(field.Name, "Rad", "Field name incorrect"); - Expect.equal(field.Op, Op.NEX, "Operator incorrect"); - }), - TestList("NameToPath", - [ - TestCase("succeeds for PostgreSQL and a simple name", () => - { - Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), - "Path not constructed correctly"); - }), - TestCase("succeeds for SQLite and a simple name", () => - { - Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), - "Path not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL and a nested name", () => - { - Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", - Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), - "Path not constructed correctly"); - }), - TestCase("succeeds for SQLite and a nested name", () => - { - Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", - Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), - "Path not constructed correctly"); - }) - ]), - 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("Path", - [ - TestCase("succeeds for a PostgreSQL single field with no qualifier", () => - { - var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL single field with a qualifier", () => - { - var field = Field.LT("SomethingElse", 9).WithQualifier("this"); - Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => - { - var field = Field.EQ("My.Nested.Field", "howdy"); - Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => - { - var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); - Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a SQLite single field with no qualifier", () => - { - var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite single field with a qualifier", () => - { - var field = Field.LT("SomethingElse", 9).WithQualifier("this"); - Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite nested field with no qualifier", () => - { - var field = Field.EQ("My.Nested.Field", "howdy"); - Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite nested field with a qualifier", () => - { - var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); - Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }) - ]) - ]), - TestList("FieldMatch.ToString", - [ - TestCase("succeeds for Any", () => - { - Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); - }), - TestCase("succeeds for All", () => - { - Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); - }) - ]), - TestList("ParameterName.Derive", - [ - TestCase("succeeds with existing name", () => - { - ParameterName name = new(); - Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); - Expect.equal(name.Derive(FSharpOption.None), "@field0", - "Counter should not have advanced for named field"); - }), - TestCase("Derive succeeds with non-existent name", () => - { - ParameterName name = new(); - Expect.equal(name.Derive(FSharpOption.None), "@field0", - "Anonymous field name should have been returned"); - Expect.equal(name.Derive(FSharpOption.None), "@field1", - "Counter should have advanced from previous call"); - Expect.equal(name.Derive(FSharpOption.None), "@field2", - "Counter should have advanced from previous call"); - Expect.equal(name.Derive(FSharpOption.None), "@field3", - "Counter should have advanced from previous call"); - }) - ]), - TestList("AutoId", - [ - TestCase("GenerateGuid succeeds", () => - { - var autoId = AutoId.GenerateGuid(); - Expect.isNotNull(autoId, "The GUID auto-ID should not have been null"); - Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long"); - Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase"); - }), - TestCase("GenerateRandomString succeeds", () => - { - foreach (var length in (int[])[6, 8, 12, 20, 32, 57, 64]) - { - var autoId = AutoId.GenerateRandomString(length); - Expect.isNotNull(autoId, $"Random string ({length}) should not have been null"); - Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long"); - Expect.equal(autoId, autoId.ToLowerInvariant(), - $"Random string ({length}) should have been lowercase"); - } - }) - ]), - TestList("Query", - [ - TestCase("StatementWhere succeeds", () => - { - Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); - }), - 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", - [ - TestCase("succeeds when a schema is present", () => - { - Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", - "CREATE INDEX for key statement with schema not constructed correctly"); - }), - TestCase("succeeds when a schema is not present", () => - { - Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", - "CREATE INDEX for key statement without schema not constructed correctly"); - }) - ]), - TestList("EnsureIndexOn", - [ - TestCase("succeeds for multiple fields and directions", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("test.table", "gibberish", - ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", - "CREATE INDEX for multiple field statement incorrect"); - }), - TestCase("succeeds for nested PostgreSQL field", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), - "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", - "CREATE INDEX for nested PostgreSQL field incorrect"); - }), - TestCase("succeeds for nested SQLite field", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), - "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", - "CREATE INDEX for nested SQLite field incorrect"); - }) - ]) - ]), - TestCase("Insert succeeds", () => - { - Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); - }), - TestCase("Save succeeds", () => - { - Expect.equal(Query.Save("tbl"), - "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", - "INSERT ON CONFLICT UPDATE statement not correct"); - }), - TestCase("Count succeeds", () => - { - Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); - }), - TestCase("Exists succeeds", () => - { - Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", - "Exists query not correct"); - }), - TestCase("Find succeeds", () => - { - Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); - }), - TestCase("Update succeeds", () => - { - Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); - }), - TestCase("Delete succeeds", () => - { - Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); - }), - TestList("OrderBy", - [ - TestCase("succeeds for no fields", () => - { - Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", - "Order By should have been blank (PostgreSQL)"); - Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); - }), - TestCase("succeeds for PostgreSQL with one field and no direction", () => - { - Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), - " ORDER BY data->>'TestField'", "Order By not constructed correctly"); - }), - TestCase("succeeds for SQLite with one field and no direction", () => - { - Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), - " ORDER BY data->>'TestField'", "Order By not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL with multiple fields and direction", () => - { - Expect.equal( - Query.OrderBy( - [ - Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), - Field.Named("It DESC") - ], - Dialect.PostgreSQL), - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", - "Order By not constructed correctly"); - }), - TestCase("succeeds for SQLite with multiple fields and direction", () => - { - Expect.equal( - Query.OrderBy( - [ - Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), - Field.Named("It DESC") - ], - Dialect.SQLite), - " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", - "Order By not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL numeric fields", () => - { - Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), - " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); - }), - TestCase("succeeds for SQLite numeric fields", () => - { - Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", - "Order By not constructed correctly for numeric field"); - }) - ]) - ]) + OpTests, + FieldTests, + FieldMatchTests, + ParameterNameTests, + AutoIdTests, + QueryTests, + TestSequenced(ConfigurationTests) ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 38324af..a224094 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -170,7 +170,11 @@ public static class PostgresCSharpTests Expect.isTrue(false, "The parameter was not a StringArray type"); } }) - ]) + ]), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + }) ]); /// @@ -310,15 +314,6 @@ public static class PostgresCSharpTests }) ]); - /// - /// Tests which do not hit the database - /// - private static readonly Test Unit = TestList("Unit", - [ - ParametersTests, - QueryTests - ]); - private static readonly List TestDocuments = [ new() { Id = "one", Value = "FIRST!", NumValue = 0 }, @@ -548,6 +543,83 @@ public static class PostgresCSharpTests { // This is what should have happened } + }), + TestCase("succeeds when adding a numeric auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } + }), + TestCase("succeeds when adding a GUID auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Guid); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) ]), TestList("Save", diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index b453ff8..2ea73ca 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -325,9 +325,86 @@ public static class SqliteCSharpTests { // This is what is supposed to happen } + }), + TestCase("succeeds when adding a numeric auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } + }), + TestCase("succeeds when adding a GUID auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Guid); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) ]), - TestList("Document.Save", + TestList("Save", [ TestCase("succeeds when a document is inserted", async () => { diff --git a/src/Tests.CSharp/Types.cs b/src/Tests.CSharp/Types.cs index 5e7f972..ce63230 100644 --- a/src/Tests.CSharp/Types.cs +++ b/src/Tests.CSharp/Types.cs @@ -1,5 +1,11 @@ namespace BitBadger.Documents.Tests.CSharp; +public class NumIdDocument +{ + public int Key { get; set; } = 0; + public string Text { get; set; } = ""; +} + public class SubDocument { public string Foo { get; set; } = ""; diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index d976fb3..a362ea2 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -6,8 +6,8 @@ - + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 46b69c9..ad0da18 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -217,6 +217,115 @@ let autoIdTests = testList "AutoId" [ Expect.stringHasLength autoId length $"Random string should have been {length} characters long" Expect.equal autoId (autoId.ToLowerInvariant ()) $"Random string ({length}) should have been lowercase") } + testList "NeedsAutoId" [ + test "succeeds when no auto ID is configured" { + Expect.isFalse (AutoId.NeedsAutoId Disabled (obj ()) "id") "Disabled auto-ID never needs an automatic ID" + } + test "fails for any when the ID property is not found" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Number {| Key = "" |} "Id" |> ignore) + "Non-existent ID property should have thrown an exception" + } + test "succeeds for byte when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int8 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for byte when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int8 4 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for short when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int16 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for short when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int16 7 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for int when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for int when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 32 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for long when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0L |} "Id") "Zero ID should have returned true" + } + test "succeeds for long when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 80L |} "Id") "Non-zero ID should have returned false" + } + test "fails for number when the ID is not a number" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Number {| Id = "" |} "Id" |> ignore) + "Numeric ID against a string should have thrown an exception" + } + test "succeeds for GUID when the ID is blank" { + Expect.isTrue (AutoId.NeedsAutoId Guid {| Id = "" |} "Id") "Blank ID should have returned true" + } + test "succeeds for GUID when the ID is filled" { + Expect.isFalse (AutoId.NeedsAutoId Guid {| Id = "abc" |} "Id") "Filled ID should have returned false" + } + test "fails for GUID when the ID is not a string" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Guid {| Id = 8 |} "Id" |> ignore) + "String ID against a number should have thrown an exception" + } + test "succeeds for RandomString when the ID is blank" { + Expect.isTrue (AutoId.NeedsAutoId RandomString {| Id = "" |} "Id") "Blank ID should have returned true" + } + test "succeeds for RandomString when the ID is filled" { + Expect.isFalse (AutoId.NeedsAutoId RandomString {| Id = "x" |} "Id") "Filled ID should have returned false" + } + test "fails for RandomString when the ID is not a string" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId RandomString {| Id = 33 |} "Id" |> ignore) + "String ID against a number should have thrown an exception" + } + ] +] + +/// Unit tests for the Configuration module +let configurationTests = testList "Configuration" [ + test "useSerializer succeeds" { + try + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" + member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> + } + + let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} + Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" + + let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" + Expect.isNull deserialized "Specified serializer should have returned null" + finally + Configuration.useSerializer DocumentSerializer.``default`` + } + test "serializer returns configured serializer" { + Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) + "Serializer should have been the same" + } + test "useIdField / idField succeeds" { + try + Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" + Configuration.useIdField "id" + Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" + finally + Configuration.useIdField "Id" + } + test "useAutoIdStrategy / autoIdStrategy succeeds" { + try + Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect" + Configuration.useAutoIdStrategy Guid + Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly" + finally + Configuration.useAutoIdStrategy Disabled + } + test "useIdStringLength / idStringLength succeeds" { + try + Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect" + Configuration.useIdStringLength 33 + Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly" + finally + Configuration.useIdStringLength 16 + } ] /// Unit tests for the Query module @@ -344,54 +453,6 @@ let queryTests = testList "Query" [ ] ] -/// Unit tests for the Configuration module -let configurationTests = testList "Configuration" [ - test "useSerializer succeeds" { - try - Configuration.useSerializer - { new IDocumentSerializer with - member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" - member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> - } - - let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} - Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" - - let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" - Expect.isNull deserialized "Specified serializer should have returned null" - finally - Configuration.useSerializer DocumentSerializer.``default`` - } - test "serializer returns configured serializer" { - Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) - "Serializer should have been the same" - } - test "useIdField / idField succeeds" { - try - Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" - Configuration.useIdField "id" - Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" - finally - Configuration.useIdField "Id" - } - test "useAutoIdStrategy / autoIdStrategy succeeds" { - try - Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect" - Configuration.useAutoIdStrategy Guid - Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly" - finally - Configuration.useAutoIdStrategy Disabled - } - test "useIdStringLength / idStringLength succeeds" { - try - Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect" - Configuration.useIdStringLength 33 - Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly" - finally - Configuration.useIdStringLength 16 - } -] - /// Tests which do not hit the database let all = testList "Common" [ opTests diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index ee31a27..497e712 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -252,18 +252,9 @@ let queryTests = testList "Query" [ open ThrowawayDb.Postgres open Types -/// Documents to use for integration tests -let documents = [ - { 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 } -] - /// Load the test documents into the database let loadDocs () = backgroundTask { - for doc in documents do do! insert PostgresDb.TableName doc + for doc in testDocuments do do! insert PostgresDb.TableName doc } /// Integration tests for the Configuration module of the PostgreSQL library @@ -435,6 +426,68 @@ let documentTests = testList "Document" [ |> Async.RunSynchronously) "An exception should have been raised for duplicate document ID insert" } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { Key = 0; Text = "one" } + do! insert PostgresDb.TableName { Key = 0; Text = "two" } + do! insert PostgresDb.TableName { Key = 77; Text = "three" } + do! insert PostgresDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered PostgresDb.TableName [ Field.Named "n:Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } ] testList "save" [ testTask "succeeds when a document is inserted" { diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 16b91eb..ebf0293 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -118,18 +118,9 @@ let parametersTests = testList "Parameters" [ (** INTEGRATION TESTS **) -/// Documents used for integration tests -let documents = [ - { 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 } -] - /// Load a table with the test documents let loadDocs () = backgroundTask { - for doc in documents do do! insert SqliteDb.TableName doc + for doc in testDocuments do do! insert SqliteDb.TableName doc } /// Integration tests for the Configuration module of the SQLite library @@ -283,6 +274,68 @@ let documentTests = testList "Document" [ insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) "An exception should have been raised for duplicate document ID insert" } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { Key = 0; Text = "one" } + do! insert SqliteDb.TableName { Key = 0; Text = "two" } + do! insert SqliteDb.TableName { Key = 77; Text = "three" } + do! insert SqliteDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered SqliteDb.TableName [ Field.Named "Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } ] testList "save" [ testTask "succeeds when a document is inserted" { diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index de2abca..0deb585 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -1,5 +1,9 @@ module Types +type NumIdDocument = + { Key: int + Text: string } + type SubDocument = { Foo: string Bar: string } -- 2.45.1 From dc60c25ac6746a7342f2a6a329cbb074575e8e9a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 19 Aug 2024 19:17:54 -0400 Subject: [PATCH 28/29] Add rc1 to version --- src/Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4796c0a..58be66d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,6 +6,7 @@ 4.0.0.0 4.0.0.0 4.0.0 + rc1 Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility danieljsummers Bit Badger Solutions -- 2.45.1 From 3ef7b25426a2024fd48a05252c3dc13e23614dbe Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 19 Aug 2024 19:23:21 -0400 Subject: [PATCH 29/29] Remove old packages before building new --- src/package.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/package.sh b/src/package.sh index d88421a..307f3d2 100755 --- a/src/package.sh +++ b/src/package.sh @@ -1,13 +1,16 @@ #!/bin/bash echo --- Package Common library +rm Common/bin/Release/BitBadger.Documents.Common.*.nupkg || true dotnet pack Common/BitBadger.Documents.Common.fsproj -c Release cp Common/bin/Release/BitBadger.Documents.Common.*.nupkg . echo --- Package PostgreSQL library +rm Postgres/bin/Release/BitBadger.Documents.Postgres*.nupkg || true dotnet pack Postgres/BitBadger.Documents.Postgres.fsproj -c Release cp Postgres/bin/Release/BitBadger.Documents.Postgres.*.nupkg . echo --- Package SQLite library +rm Sqlite/bin/Release/BitBadger.Documents.Sqlite*.nupkg || true dotnet pack Sqlite/BitBadger.Documents.Sqlite.fsproj -c Release cp Sqlite/bin/Release/BitBadger.Documents.Sqlite.*.nupkg . -- 2.45.1