diff --git a/src/Common/BitBadger.Documents.Common.fsproj b/src/Common/BitBadger.Documents.Common.fsproj index bded7fe..6e91b44 100644 --- a/src/Common/BitBadger.Documents.Common.fsproj +++ b/src/Common/BitBadger.Documents.Common.fsproj @@ -13,7 +13,8 @@ - + + diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 56b6d1b..77dc4c1 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -15,6 +15,8 @@ type Op = | LE /// Not Equal to (<>) | NE + /// Between (BETWEEN) + | BT /// Exists (IS NOT NULL) | EX /// Does Not Exist (IS NULL) @@ -28,6 +30,7 @@ type Op = | LT -> "<" | LE -> "<=" | NE -> "<>" + | BT -> "BETWEEN" | EX -> "IS NOT NULL" | NEX -> "IS NULL" @@ -68,11 +71,15 @@ type Field = { static member NE name (value: obj) = { Name = name; Op = NE; Value = value } + /// Create a BETWEEN field criterion + static member BT name (min: obj) (max: obj) = + { Name = name; Op = BT; Value = [ min; max ] } + /// Create an exists (IS NOT NULL) field criterion static member EX name = { Name = name; Op = EX; Value = obj () } - /// Create an not exists (IS NULL) field criterion + /// Create a not exists (IS NULL) field criterion static member NEX name = { Name = name; Op = NEX; Value = obj () } @@ -150,17 +157,6 @@ module Query = let selectFromTable tableName = $"SELECT data FROM %s{tableName}" - /// 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 -> string field.Op | _ -> $"{field.Op} %s{paramName}" - $"data ->> '%s{field.Name}' {theRest}" - - /// Create a WHERE clause fragment to implement an ID-based query - [] - let whereById paramName = - whereByField (Field.EQ (Configuration.idField ()) 0) paramName - /// Queries to define tables and indexes module Definition = @@ -202,62 +198,6 @@ module Query = [] let save tableName = sprintf - "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" + "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - - /// 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 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 a JSON field - [] - let byField tableName field = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@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 a JSON field - [] - let byField tableName field = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" - - /// Queries for retrieving documents - module Find = - - /// Query to retrieve a document by its ID - [] - let byId tableName = - $"""{selectFromTable tableName} WHERE {whereById "@id"}""" - - /// Query to retrieve documents using a comparison on a JSON field - [] - let byField tableName field = - $"""{selectFromTable tableName} WHERE {whereByField field "@field"}""" - - /// 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 a JSON field - [] - let byField tableName field = - $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e8389bc..ea70aa3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,11 +1,12 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 embedded false - 3.0.0.0 - 3.0.0.0 - 3.0.0 + 3.1.0.0 + 3.1.0.0 + 3.1.0 + Add BT (between) operator; drop .NET 7 support danieljsummers Bit Badger Solutions README.md diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index a8eb158..316d2ae 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -15,6 +15,7 @@ + diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 1351af8..cd551d4 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -63,17 +63,29 @@ module Parameters = let jsonParam (name: string) (it: 'TJson) = name, Sql.jsonb (Configuration.serializer().Serialize it) - /// Create a JSON field parameter (name "@field") + /// 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 - /// Create a JSON field parameter (name "@field") + /// 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 /// Append JSON field name parameters for the given field names to the given parameters @@ -97,6 +109,25 @@ module Parameters = [] module Query = + /// 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}" + + /// Create a WHERE clause fragment to implement an ID-based query + [] + let whereById paramName = + whereByField (Field.EQ (Configuration.idField ()) 0) paramName + /// Table and index definition queries module Definition = @@ -112,6 +143,11 @@ 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 = @@ -125,6 +161,16 @@ 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 a JSON field + [] + let byField tableName field = + $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}""" + /// Query to count matching documents using a JSON containment query (@>) [] let byContains tableName = @@ -138,6 +184,16 @@ module Query = /// 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 a JSON field + [] + let byField tableName field = + $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" + /// Query to determine if documents exist using a JSON containment query (@>) [] let byContains tableName = @@ -151,6 +207,16 @@ module Query = /// 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 a JSON field + [] + let byField tableName field = + $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}""" + /// Query to retrieve documents using a JSON containment query (@>) [] let byContains tableName = @@ -171,12 +237,12 @@ module Query = /// Query to patch a document by its ID [] let byId tableName = - Query.whereById "@id" |> update tableName + whereById "@id" |> update tableName /// Query to patch documents match a JSON field comparison (->> =) [] let byField tableName field = - Query.whereByField field "@field" |> update tableName + whereByField field "@field" |> update tableName /// Query to patch documents matching a JSON containment query (@>) [] @@ -198,12 +264,12 @@ module Query = /// Query to remove fields from a document by the document's ID [] let byId tableName = - Query.whereById "@id" |> update tableName + whereById "@id" |> update tableName /// Query to remove fields from documents via a comparison on a JSON field within the document [] let byField tableName field = - Query.whereByField field "@field" |> update tableName + whereByField field "@field" |> update tableName /// Query to patch documents matching a JSON containment query (@>) [] @@ -218,6 +284,16 @@ module Query = /// 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 a JSON field + [] + let byField tableName field = + $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" + /// Query to delete documents using a JSON containment query (@>) [] let byContains tableName = diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index b6009bb..14a006d 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -14,7 +14,8 @@ - + + diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 8e7cdb7..27815f8 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -31,6 +31,21 @@ module Configuration = [] module Query = + /// 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}" + + /// Create a WHERE clause fragment to implement an ID-based query + [] + let whereById paramName = + whereByField (Field.EQ (Configuration.idField ()) 0) paramName + /// Data definition module Definition = @@ -39,6 +54,50 @@ 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 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 a JSON field + [] + let byField tableName field = + $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@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 a JSON field + [] + let byField tableName field = + $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" + + /// 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 a JSON field + [] + let byField tableName field = + $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}""" + /// Document patching (partial update) queries module Patch = @@ -49,12 +108,12 @@ module Query = /// Query to patch (partially update) a document by its ID [] let byId tableName = - Query.whereById "@id" |> update tableName + whereById "@id" |> update tableName /// Query to patch (partially update) a document via a comparison on a JSON field [] let byField tableName field = - Query.whereByField field "@field" |> update tableName + whereByField field "@field" |> update tableName /// Queries to remove fields from documents module RemoveFields = @@ -67,7 +126,7 @@ module Query = /// Query to remove fields from a document by the document's ID [] let byId tableName parameters = - Query.whereById "@id" |> update tableName parameters + whereById "@id" |> update tableName parameters /// Query to remove fields from a document by the document's ID let ById(tableName, parameters) = @@ -76,12 +135,25 @@ module Query = /// Query to remove fields from documents via a comparison on a JSON field within the document [] let byField tableName field parameters = - Query.whereByField field "@field" |> update tableName parameters + whereByField field "@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) + /// 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 a JSON field + [] + let byField tableName field = + $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" + /// Parameter handling helpers [] @@ -100,12 +172,26 @@ module Parameters = /// Create a JSON field parameter (name "@field") [] let addFieldParam name field parameters = - match field.Op with EX | NEX -> parameters | _ -> SqliteParameter(name, field.Value) :: 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 /// 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 /// Append JSON field name parameters for the given field names to the given parameters diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index 489d298..951b6d7 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 1426c3c..fd8162a 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -1,5 +1,6 @@ using Expecto.CSharp; using Expecto; +using Microsoft.FSharp.Collections; namespace BitBadger.Documents.Tests.CSharp; @@ -96,6 +97,10 @@ public static class CommonCSharpTests { 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"); @@ -149,6 +154,13 @@ public static class CommonCSharpTests 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(), new object[] { 18, 49 }, "Value incorrect"); + }), TestCase("EX succeeds", () => { var field = Field.EX("Groovy"); @@ -169,23 +181,6 @@ public static class CommonCSharpTests Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", "SELECT statement not correct"); }), - TestCase("WhereById succeeds", () => - { - Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); - }), - TestList("WhereByField", new[] - { - TestCase("succeeds when a logical operator is passed", () => - { - Expect.equal(Query.WhereByField(Field.GT("theField", 0), "@test"), "data ->> 'theField' > @test", - "WHERE clause not correct"); - }), - TestCase("succeeds when an existence operator is passed", () => - { - Expect.equal(Query.WhereByField(Field.NEX("thatField"), ""), "data ->> 'thatField' IS NULL", - "WHERE clause not correct"); - }) - }), TestList("Definition", new[] { TestCase("EnsureTableFor succeeds", () => @@ -226,69 +221,8 @@ public static class CommonCSharpTests TestCase("Save succeeds", () => { Expect.equal(Query.Save("tbl"), - $"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data", + "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); - }), - TestCase("Update succeeds", () => - { - Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", - "UPDATE full statement not correct"); - }), - TestList("Count", new[] - { - TestCase("All succeeds", () => - { - Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Count.ByField("tbl", Field.EQ("thatField", 0)), - "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", - "JSON field text comparison count query not correct"); - }) - }), - TestList("Exists", new[] - { - TestCase("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"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Exists.ByField("tbl", Field.LT("Test", 0)), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", - "JSON field text comparison exists query not correct"); - }) - }), - TestList("Find", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", - "SELECT by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Find.ByField("tbl", Field.GE("Golf", 0)), - "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", - "SELECT by JSON comparison query not correct"); - }) - }), - TestList("Delete", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", - "DELETE by ID query not correct"); - }), - TestCase("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"); - }) }) }) }); diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 1d6c158..9ddd720 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -47,42 +47,48 @@ public static class PostgresCSharpTests { var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty>()); 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(); + 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("RemoveFields", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ById("tbl"), - "UPDATE tbl SET data = data - @name WHERE data ->> 'Id' = @id", - "Remove field by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0)), - "UPDATE tbl SET data = data - @name WHERE data ->> 'Fly' < @field", - "Remove field by field query not correct"); - }), - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByContains("tbl"), - "UPDATE tbl SET data = data - @name WHERE data @> @criteria", - "Remove field by contains query not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByJsonPath("tbl"), - "UPDATE tbl SET data = data - @name WHERE data @? @path::jsonpath", - "Remove field by JSON path query not correct"); - }) - }), - TestCase("None succeeds", () => - { - Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); }) }), TestList("Query", new[] { + TestList("WhereByField", new[] + { + 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"); + }) + }), + TestCase("WhereById succeeds", () => + { + Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); + }), TestList("Definition", new[] { TestCase("EnsureTable succeeds", () => @@ -107,6 +113,11 @@ 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", @@ -119,6 +130,17 @@ public static class PostgresCSharpTests }), TestList("Count", new[] { + TestCase("All succeeds", () => + { + Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), + $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct"); + }), + 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", + "JSON field text comparison count query not correct"); + }), TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), @@ -134,6 +156,18 @@ public static class PostgresCSharpTests }), TestList("Exists", new[] { + 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("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", + "JSON field text comparison exists query not correct"); + }), TestCase("ByContains succeeds", () => { Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), @@ -149,6 +183,18 @@ public static class PostgresCSharpTests }), TestList("Find", new[] { + 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("ByField succeeds", () => + { + Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)), + $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field", + "SELECT by JSON comparison query not correct"); + }), TestCase("byContains succeeds", () => { Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), @@ -167,13 +213,13 @@ public static class PostgresCSharpTests TestCase("ById succeeds", () => { Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id", "UPDATE partial by ID statement not correct"); }), 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' < @field", "UPDATE partial by ID statement not correct"); }), TestCase("ByContains succeeds", () => @@ -189,8 +235,47 @@ public static class PostgresCSharpTests "UPDATE partial by JSON Path statement not correct"); }) }), + TestList("RemoveFields", new[] + { + 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("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", + "Remove field by field query not correct"); + }), + 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", new[] { + 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("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"); + }), TestCase("byContains succeeds", () => { Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), @@ -464,13 +549,21 @@ public static class PostgresCSharpTests var theCount = await Count.All(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), - TestCase("ByField succeeds", async () => + TestCase("ByField succeeds for numeric range", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var theCount = await Count.ByField(PostgresDb.TableName, Field.EQ("Value", "purple")); - Expect.equal(theCount, 2, "There should have been 2 matching documents"); + 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.ByField(PostgresDb.TableName, Field.BT("Value", "aardvark", "apple")); + Expect.equal(theCount, 1, "There should have been 1 matching document"); }), TestCase("ByContains succeeds", async () => { diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index be035e8..6f38664 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -21,23 +21,93 @@ public static class SqliteCSharpTests { TestList("Query", new[] { + TestList("WhereByField", new[] + { + 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"); + }) + }), + TestCase("WhereById succeeds", () => + { + Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause 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"); }), + 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", new[] + { + TestCase("All succeeds", () => + { + Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", + "Count query not correct"); + }), + TestCase("ByField succeeds", () => + { + Expect.equal(Sqlite.Query.Count.ByField("tbl", Field.EQ("thatField", 0)), + "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field", + "JSON field text comparison count query not correct"); + }) + }), + TestList("Exists", new[] + { + 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"); + }), + 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", + "JSON field text comparison exists query not correct"); + }) + }), + TestList("Find", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", + "SELECT by ID query not correct"); + }), + TestCase("ByField succeeds", () => + { + Expect.equal(Sqlite.Query.Find.ByField("tbl", Field.GE("Golf", 0)), + "SELECT data FROM tbl WHERE data->>'Golf' >= @field", + "SELECT by JSON comparison query not correct"); + }) + }), TestList("Patch", new[] { TestCase("ById succeeds", () => { Expect.equal(Sqlite.Query.Patch.ById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id", "UPDATE partial by ID statement not correct"); }), 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' <> @field", "UPDATE partial by JSON comparison query not correct"); }) }), @@ -46,16 +116,29 @@ public static class SqliteCSharpTests 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", + "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id", "Remove field by ID query not correct"); }), 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' < @field", "Remove field by field query not correct"); }) + }), + TestList("Delete", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", + "DELETE by ID query not correct"); + }), + 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"); + }) }) }), TestList("Parameters", new[] @@ -316,13 +399,21 @@ public static class SqliteCSharpTests var theCount = await Count.All(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), - TestCase("ByField succeeds", async () => + TestCase("ByField succeeds for numeric range", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var theCount = await Count.ByField(SqliteDb.TableName, Field.EQ("Value", "purple")); - Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + 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.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple")); + Expect.equal(theCount, 1L, "There should have been 1 matching document"); }) }), TestList("Exists", new[] diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index 9e753ad..90c8b65 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -15,7 +15,8 @@ - + + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index dd0ac97..c5265ea 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -28,6 +28,9 @@ let all = 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""" } @@ -35,27 +38,64 @@ let all = 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" + } + 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" + } + 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" + } + 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" + } + 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" + } + 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" + } + 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" + } + test "EX succeeds" { + let field = Field.EX "Groovy" + Expect.equal field.Name "Groovy" "Field name incorrect" + Expect.equal field.Op EX "Operator incorrect" + } + test "NEX succeeds" { + let field = Field.NEX "Rad" + Expect.equal field.Name "Rad" "Field name incorrect" + Expect.equal field.Op NEX "Operator incorrect" + } + ] testList "Query" [ test "selectFromTable succeeds" { Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" } - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "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" - } - ] testList "Definition" [ test "ensureTableFor succeeds" { Expect.equal @@ -92,68 +132,9 @@ let all = test "save succeeds" { Expect.equal (Query.save tbl) - $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" + $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" "INSERT ON CONFLICT UPDATE 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 "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)) - $"SELECT COUNT(*) AS it FROM {tbl} WHERE data ->> 'thatField' = @field" - "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' < @field) AS it" - "JSON field text comparison exists query 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' >= @field" - "SELECT by JSON comparison 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" - } - ] ] ] diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 29384ec..6849b8b 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -34,12 +34,59 @@ let unitTests = 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 min = paramList[0] + 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] + 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" + } ] test "noParams succeeds" { Expect.isEmpty noParams "The no-params sequence should be empty" } ] testList "Query" [ + 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" + } testList "Definition" [ test "ensureTable succeeds" { Expect.equal @@ -61,6 +108,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" } @@ -68,6 +121,18 @@ 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 "byField succeeds" { + Expect.equal + (Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0)) + $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field" + "JSON field text comparison count query not correct" + } test "byContains succeeds" { Expect.equal (Query.Count.byContains PostgresDb.TableName) @@ -82,6 +147,18 @@ let unitTests = } ] 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 "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" + "JSON field text comparison exists query not correct" + } test "byContains succeeds" { Expect.equal (Query.Exists.byContains PostgresDb.TableName) @@ -96,6 +173,18 @@ let unitTests = } ] 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 "byField succeeds" { + Expect.equal + (Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0)) + $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field" + "SELECT by JSON comparison query not correct" + } test "byContains succeeds" { Expect.equal (Query.Find.byContains PostgresDb.TableName) @@ -113,13 +202,13 @@ let unitTests = test "byId succeeds" { Expect.equal (Query.Patch.byId PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id" "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' < @field" + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field" "UPDATE partial by ID statement not correct" } test "byContains succeeds" { @@ -139,13 +228,13 @@ let unitTests = test "byId succeeds" { Expect.equal (Query.RemoveFields.byId "tbl") - "UPDATE tbl SET data = data - @name WHERE data ->> 'Id' = @id" + "UPDATE tbl SET data = data - @name WHERE data->>'Id' = @id" "Remove field by ID 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' < @field" + "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field" "Remove field by field query not correct" } test "byContains succeeds" { @@ -162,6 +251,18 @@ let unitTests = } ] 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 "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" @@ -387,12 +488,19 @@ let integrationTests = let! theCount = Count.all PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - testTask "byField succeeds" { + testTask "byField succeeds for numeric range" { use db = PostgresDb.BuildDb() do! loadDocs () - let! theCount = Count.byField PostgresDb.TableName (Field.EQ "Value" "purple") - Expect.equal theCount 2 "There should have been 2 matching documents" + 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" } testTask "byContains succeeds" { use db = PostgresDb.BuildDb() diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 9063d2e..6d97893 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -12,23 +12,91 @@ open Types let unitTests = testList "Unit" [ testList "Query" [ + 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" + } 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" } + 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 "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)) + "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field" + "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' < @field) AS it" + "JSON field text comparison exists query 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' >= @field" + "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 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' <> @field" + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field" "UPDATE partial by JSON comparison query not correct" } ] @@ -36,7 +104,7 @@ let unitTests = test "byId succeeds" { Expect.equal (Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ]) - "UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id" + "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id" "Remove field by ID query not correct" } test "byField succeeds" { @@ -45,10 +113,24 @@ 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' > @field" "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" { @@ -299,12 +381,19 @@ let integrationTests = let! theCount = Count.all SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } - testTask "byField succeeds" { + testTask "byField succeeds for a numeric range" { use! db = SqliteDb.BuildDb() do! loadDocs () - let! theCount = Count.byField SqliteDb.TableName (Field.EQ "Value" "purple") - Expect.equal theCount 2L "There should have been 2 matching documents" + 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 "Exists" [ diff --git a/src/test_all.sh b/src/test_all.sh index f93110d..65a2592 100755 --- a/src/test_all.sh +++ b/src/test_all.sh @@ -8,7 +8,7 @@ cd ./Tests || exit export BBDOX_PG_PORT=8301 PG_VERSIONS=('12' '13' '14' '15' 'latest') -NET_VERSIONS=('6.0' '7.0' '8.0') +NET_VERSIONS=('6.0' '8.0') for PG_VERSION in "${PG_VERSIONS[@]}" do