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")