Changes for 3.1 #5

Merged
danieljsummers merged 3 commits from 3.1 into main 2024-06-06 01:45:36 +00:00
16 changed files with 706 additions and 303 deletions

View File

@ -13,7 +13,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.2.42" /> <PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -15,6 +15,8 @@ type Op =
| LE | LE
/// Not Equal to (<>) /// Not Equal to (<>)
| NE | NE
/// Between (BETWEEN)
| BT
/// Exists (IS NOT NULL) /// Exists (IS NOT NULL)
| EX | EX
/// Does Not Exist (IS NULL) /// Does Not Exist (IS NULL)
@ -28,6 +30,7 @@ type Op =
| LT -> "<" | LT -> "<"
| LE -> "<=" | LE -> "<="
| NE -> "<>" | NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL" | EX -> "IS NOT NULL"
| NEX -> "IS NULL" | NEX -> "IS NULL"
@ -68,11 +71,15 @@ type Field = {
static member NE name (value: obj) = static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value } { 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 /// Create an exists (IS NOT NULL) field criterion
static member EX name = static member EX name =
{ Name = name; Op = EX; Value = obj () } { 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 = static member NEX name =
{ Name = name; Op = NEX; Value = obj () } { Name = name; Op = NEX; Value = obj () }
@ -150,17 +157,6 @@ module Query =
let selectFromTable tableName = let selectFromTable tableName =
$"SELECT data FROM %s{tableName}" $"SELECT data FROM %s{tableName}"
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
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
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Queries to define tables and indexes /// Queries to define tables and indexes
module Definition = module Definition =
@ -202,62 +198,6 @@ module Query =
[<CompiledName "Save">] [<CompiledName "Save">]
let save tableName = let save tableName =
sprintf 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 ()) tableName (Configuration.idField ())
/// Query to update a document
[<CompiledName "Update">]
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
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
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
[<CompiledName "ById">]
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
[<CompiledName "ByField">]
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
[<CompiledName "ById">]
let byId tableName =
$"""{selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Queries to delete documents
module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""

View File

@ -1,11 +1,12 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.1.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.1.0.0</FileVersion>
<VersionPrefix>3.0.0</VersionPrefix> <VersionPrefix>3.1.0</VersionPrefix>
<PackageReleaseNotes>Add BT (between) operator; drop .NET 7 support</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>

View File

@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" /> <PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -63,17 +63,29 @@ module Parameters =
let jsonParam (name: string) (it: 'TJson) = let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it) name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field") /// Create a JSON field parameter
[<CompiledName "FSharpAddField">] [<CompiledName "FSharpAddField">]
let addFieldParam name field parameters = let addFieldParam name field parameters =
match field.Op with match field.Op with
| EX | NEX -> parameters | 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 | _ -> (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 = let AddField name field parameters =
match field.Op with match field.Op with
| EX | NEX -> parameters | 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 | _ -> (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 /// Append JSON field name parameters for the given field names to the given parameters
@ -97,6 +109,25 @@ module Parameters =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
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
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Table and index definition queries /// Table and index definition queries
module Definition = module Definition =
@ -112,6 +143,11 @@ module Query =
let tableName = name.Split '.' |> Array.last let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
/// Query to update a document
[<CompiledName "Update">]
let update tableName =
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition /// Create a WHERE clause fragment to implement a @> (JSON contains) condition
[<CompiledName "WhereDataContains">] [<CompiledName "WhereDataContains">]
let whereDataContains paramName = let whereDataContains paramName =
@ -125,6 +161,16 @@ module Query =
/// Queries for counting documents /// Queries for counting documents
module Count = module Count =
/// Query to count all documents in a table
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
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 (@>) /// Query to count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@ -138,6 +184,16 @@ module Query =
/// Queries for determining document existence /// Queries for determining document existence
module Exists = module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
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
[<CompiledName "ByField">]
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 (@>) /// Query to determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@ -151,6 +207,16 @@ module Query =
/// Queries for retrieving documents /// Queries for retrieving documents
module Find = module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Query to retrieve documents using a JSON containment query (@>) /// Query to retrieve documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@ -171,12 +237,12 @@ module Query =
/// Query to patch a document by its ID /// Query to patch a document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName = let byId tableName =
Query.whereById "@id" |> update tableName whereById "@id" |> update tableName
/// Query to patch documents match a JSON field comparison (->> =) /// Query to patch documents match a JSON field comparison (->> =)
[<CompiledName "ByField">] [<CompiledName "ByField">]
let byField tableName field = let byField tableName field =
Query.whereByField field "@field" |> update tableName whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>) /// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
@ -198,12 +264,12 @@ module Query =
/// Query to remove fields from a document by the document's ID /// Query to remove fields from a document by the document's ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName = 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 /// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "ByField">] [<CompiledName "ByField">]
let byField tableName field = let byField tableName field =
Query.whereByField field "@field" |> update tableName whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>) /// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
@ -218,6 +284,16 @@ module Query =
/// Queries to delete documents /// Queries to delete documents
module Delete = module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Query to delete documents using a JSON containment query (@>) /// Query to delete documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =

View File

@ -14,7 +14,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.1" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -31,6 +31,21 @@ module Configuration =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
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
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Data definition /// Data definition
module Definition = module Definition =
@ -39,6 +54,50 @@ module Query =
let ensureTable name = let ensureTable name =
Query.Definition.ensureTableFor name "TEXT" Query.Definition.ensureTableFor name "TEXT"
/// Query to update a document
[<CompiledName "Update">]
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
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
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
[<CompiledName "ById">]
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
[<CompiledName "ByField">]
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
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Document patching (partial update) queries /// Document patching (partial update) queries
module Patch = module Patch =
@ -49,12 +108,12 @@ module Query =
/// Query to patch (partially update) a document by its ID /// Query to patch (partially update) a document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName = 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 /// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">] [<CompiledName "ByField">]
let byField tableName field = let byField tableName field =
Query.whereByField field "@field" |> update tableName whereByField field "@field" |> update tableName
/// Queries to remove fields from documents /// Queries to remove fields from documents
module RemoveFields = module RemoveFields =
@ -67,7 +126,7 @@ module Query =
/// Query to remove fields from a document by the document's ID /// Query to remove fields from a document by the document's ID
[<CompiledName "FSharpById">] [<CompiledName "FSharpById">]
let byId tableName parameters = 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 /// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) = 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 /// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "FSharpByField">] [<CompiledName "FSharpByField">]
let byField tableName field parameters = 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 /// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) = let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters) byField tableName field (List.ofSeq parameters)
/// Queries to delete documents
module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Parameter handling helpers /// Parameter handling helpers
[<AutoOpen>] [<AutoOpen>]
@ -100,12 +172,26 @@ module Parameters =
/// Create a JSON field parameter (name "@field") /// Create a JSON field parameter (name "@field")
[<CompiledName "FSharpAddField">] [<CompiledName "FSharpAddField">]
let addFieldParam name field parameters = 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") /// Create a JSON field parameter (name "@field")
let AddField(name, field, parameters) = let AddField(name, field, parameters) =
match field.Op with match field.Op with
| EX | NEX -> parameters | 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 | _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters /// Append JSON field name parameters for the given field names to the given parameters

View File

@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" /> <PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" /> <PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,5 +1,6 @@
using Expecto.CSharp; using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.FSharp.Collections;
namespace BitBadger.Documents.Tests.CSharp; 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"); 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", () => TestCase("EX succeeds", () =>
{ {
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); 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.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value 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<object>)field.Value).ToArray(), new object[] { 18, 49 }, "Value incorrect");
}),
TestCase("EX succeeds", () => TestCase("EX succeeds", () =>
{ {
var field = Field.EX("Groovy"); var field = Field.EX("Groovy");
@ -169,23 +181,6 @@ public static class CommonCSharpTests
Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table",
"SELECT statement not correct"); "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[] TestList("Definition", new[]
{ {
TestCase("EnsureTableFor succeeds", () => TestCase("EnsureTableFor succeeds", () =>
@ -226,69 +221,8 @@ public static class CommonCSharpTests
TestCase("Save succeeds", () => TestCase("Save succeeds", () =>
{ {
Expect.equal(Query.Save("tbl"), 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"); "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");
})
}) })
}) })
}); });

View File

@ -47,42 +47,48 @@ public static class PostgresCSharpTests
{ {
var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>()); var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>());
Expect.isEmpty(it, "There should not have been any parameters added"); 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<Tuple<string, SqlValue>>()).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("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[] TestList("Definition", new[]
{ {
TestCase("EnsureTable succeeds", () => TestCase("EnsureTable succeeds", () =>
@ -107,6 +113,11 @@ public static class PostgresCSharpTests
"CREATE INDEX statement not constructed correctly"); "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", () => TestCase("WhereDataContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test",
@ -119,6 +130,17 @@ public static class PostgresCSharpTests
}), }),
TestList("Count", new[] 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", () => TestCase("ByContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName),
@ -134,6 +156,18 @@ public static class PostgresCSharpTests
}), }),
TestList("Exists", new[] 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", () => TestCase("ByContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName),
@ -149,6 +183,18 @@ public static class PostgresCSharpTests
}), }),
TestList("Find", new[] 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", () => TestCase("byContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName),
@ -167,13 +213,13 @@ public static class PostgresCSharpTests
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), 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"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), 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"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByContains succeeds", () => TestCase("ByContains succeeds", () =>
@ -189,8 +235,47 @@ public static class PostgresCSharpTests
"UPDATE partial by JSON Path statement not correct"); "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[] 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", () => TestCase("byContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName),
@ -464,13 +549,21 @@ public static class PostgresCSharpTests
var theCount = await Count.All(PostgresDb.TableName); var theCount = await Count.All(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents"); 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 using var db = PostgresDb.BuildDb();
await LoadDocs(); await LoadDocs();
var theCount = await Count.ByField(PostgresDb.TableName, Field.EQ("Value", "purple")); var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 2, "There should have been 2 matching documents"); 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 () => TestCase("ByContains succeeds", async () =>
{ {

View File

@ -21,23 +21,93 @@ public static class SqliteCSharpTests
{ {
TestList("Query", new[] 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", () => TestCase("Definition.EnsureTable succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"),
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); "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[] TestList("Patch", new[]
{ {
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Patch.ById("tbl"), 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"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Patch.ByField("tbl", Field.NE("Part", 0)), 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"); "UPDATE partial by JSON comparison query not correct");
}) })
}), }),
@ -46,16 +116,29 @@ public static class SqliteCSharpTests
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }), 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"); "Remove field by ID query not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0), Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0),
new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }), 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"); "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[] TestList("Parameters", new[]
@ -316,13 +399,21 @@ public static class SqliteCSharpTests
var theCount = await Count.All(SqliteDb.TableName); var theCount = await Count.All(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents"); 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 using var db = await SqliteDb.BuildDb();
await LoadDocs(); await LoadDocs();
var theCount = await Count.ByField(SqliteDb.TableName, Field.EQ("Value", "purple")); var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 2L, "There should have been 2 matching documents"); 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[] TestList("Exists", new[]

View File

@ -15,7 +15,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" /> <PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -28,6 +28,9 @@ let all =
test "NE succeeds" { test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct" 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" { test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" 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""" 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" [ testList "Query" [
test "selectFromTable succeeds" { test "selectFromTable succeeds" {
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" 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" [ testList "Definition" [
test "ensureTableFor succeeds" { test "ensureTableFor succeeds" {
Expect.equal Expect.equal
@ -92,68 +132,9 @@ let all =
test "save succeeds" { test "save succeeds" {
Expect.equal Expect.equal
(Query.save tbl) (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" "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"
}
]
] ]
] ]

View File

@ -34,12 +34,59 @@ let unitTests =
let paramList = addFieldParam "@field" (Field.EX "tacos") [] let paramList = addFieldParam "@field" (Field.EX "tacos") []
Expect.isEmpty paramList "There should not have been any parameters added" 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" { test "noParams succeeds" {
Expect.isEmpty noParams "The no-params sequence should be empty" Expect.isEmpty noParams "The no-params sequence should be empty"
} }
] ]
testList "Query" [ 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" [ testList "Definition" [
test "ensureTable succeeds" { test "ensureTable succeeds" {
Expect.equal Expect.equal
@ -61,6 +108,12 @@ let unitTests =
"CREATE INDEX statement not constructed correctly" "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" { test "whereDataContains succeeds" {
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" 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" Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
} }
testList "Count" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Count.byContains PostgresDb.TableName) (Query.Count.byContains PostgresDb.TableName)
@ -82,6 +147,18 @@ let unitTests =
} }
] ]
testList "Exists" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Exists.byContains PostgresDb.TableName) (Query.Exists.byContains PostgresDb.TableName)
@ -96,6 +173,18 @@ let unitTests =
} }
] ]
testList "Find" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Find.byContains PostgresDb.TableName) (Query.Find.byContains PostgresDb.TableName)
@ -113,13 +202,13 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.Patch.byId PostgresDb.TableName) (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" "UPDATE partial by ID statement not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) (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" "UPDATE partial by ID statement not correct"
} }
test "byContains succeeds" { test "byContains succeeds" {
@ -139,13 +228,13 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byId "tbl") (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" "Remove field by ID query not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0)) (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" "Remove field by field query not correct"
} }
test "byContains succeeds" { test "byContains succeeds" {
@ -162,6 +251,18 @@ let unitTests =
} }
] ]
testList "Delete" [ 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" { test "byContains succeeds" {
Expect.equal (Query.Delete.byContains PostgresDb.TableName) Expect.equal (Query.Delete.byContains PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria"
@ -387,12 +488,19 @@ let integrationTests =
let! theCount = Count.all PostgresDb.TableName let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents" Expect.equal theCount 5 "There should have been 5 matching documents"
} }
testTask "byField succeeds" { testTask "byField succeeds for numeric range" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
do! loadDocs () do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName (Field.EQ "Value" "purple") let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 2 "There should have been 2 matching documents" 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" { testTask "byContains succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()

View File

@ -12,23 +12,91 @@ open Types
let unitTests = let unitTests =
testList "Unit" [ testList "Unit" [
testList "Query" [ 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" { test "Definition.ensureTable succeeds" {
Expect.equal Expect.equal
(Query.Definition.ensureTable "tbl") (Query.Definition.ensureTable "tbl")
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)"
"CREATE TABLE statement not correct" "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" [ testList "Patch" [
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.Patch.byId "tbl") (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" "UPDATE partial by ID statement not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.Patch.byField "tbl" (Field.NE "Part" 0)) (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" "UPDATE partial by JSON comparison query not correct"
} }
] ]
@ -36,7 +104,7 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ]) (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" "Remove field by ID query not correct"
} }
test "byField succeeds" { test "byField succeeds" {
@ -45,10 +113,24 @@ let unitTests =
"tbl" "tbl"
(Field.GT "Fly" 0) (Field.GT "Fly" 0)
[ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ]) [ 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" "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" [ testList "Parameters" [
test "idParam succeeds" { test "idParam succeeds" {
@ -299,12 +381,19 @@ let integrationTests =
let! theCount = Count.all SqliteDb.TableName let! theCount = Count.all SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents" 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() use! db = SqliteDb.BuildDb()
do! loadDocs () do! loadDocs ()
let! theCount = Count.byField SqliteDb.TableName (Field.EQ "Value" "purple") let! theCount = Count.byField SqliteDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 2L "There should have been 2 matching documents" 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" [ testList "Exists" [

View File

@ -8,7 +8,7 @@ cd ./Tests || exit
export BBDOX_PG_PORT=8301 export BBDOX_PG_PORT=8301
PG_VERSIONS=('12' '13' '14' '15' 'latest') 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[@]}" for PG_VERSION in "${PG_VERSIONS[@]}"
do do