Add whereByFields code / tests

- Add wrapper class for unnamed field parameters
- Support table qualifiers by field
- Support dot access to document fields/sub-fields
This commit is contained in:
Daniel J. Summers 2024-08-07 16:39:15 -04:00
parent e96c449324
commit e2232e91bb
10 changed files with 695 additions and 373 deletions

View File

@ -97,6 +97,18 @@ type Field = {
member this.WithQualifier alias = member this.WithQualifier alias =
{ this with Qualifier = Some alias } { this with Qualifier = Some alias }
/// Get the path for this field in PostgreSQL's format
member this.PgSqlPath =
(this.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data")
+ if this.Name.Contains '.' then "#>>'{" + String.concat "," (this.Name.Split '.') + "}'"
else $"->>'{this.Name}'"
/// Get the path for this field in SQLite's format
member this.SqlitePath =
(this.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data")
+ if this.Name.Contains '.' then "->>'" + String.concat "'->>'" (this.Name.Split '.') + "'"
else $"->>'{this.Name}'"
/// How fields should be matched /// How fields should be matched
[<Struct>] [<Struct>]
@ -107,6 +119,20 @@ type FieldMatch =
| All | All
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
type ParameterName() =
/// The counter for the next field value
let mutable currentIdx = -1
/// Return the specified name for the parameter, or an anonymous parameter name if none is specified
member this.Derive paramName =
match paramName with
| Some it -> it
| None ->
currentIdx <- currentIdx + 1
$"@field{currentIdx}"
/// The required document serialization implementation /// The required document serialization implementation
type IDocumentSerializer = type IDocumentSerializer =

View File

@ -110,34 +110,27 @@ module Parameters =
module Query = module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "WhereByFields">] [<CompiledName "FSharpWhereByFields">]
let whereByFields fields howMatched = let whereByFields fields howMatched =
let mutable idx = 0 let name = ParameterName()
let nameField () =
let name = $"field{idx}"
idx <- idx + 1
name
fields fields
|> List.map (fun it -> |> List.map (fun it ->
let fieldName = it.Qualifier |> Option.map (fun q -> $"{q}.data") |> Option.defaultValue "data"
let jsonPath =
if it.Name.Contains '.' then "#>>'{" + String.concat "," (it.Name.Split '.') + "}'"
else $"->>'{it.Name}'"
let column = fieldName + jsonPath
match it.Op with match it.Op with
| EX | NEX -> $"{column} {it.Op}" | EX | NEX -> $"{it.PgSqlPath} {it.Op}"
| BT -> | BT ->
let p = defaultArg it.ParameterName (nameField ()) let p = name.Derive it.ParameterName
let names = $"{p}min AND {p}max" let names = $"{p}min AND {p}max"
let values = it.Value :?> obj list let values = it.Value :?> obj list
match values[0] with match values[0] with
| :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64
| :? decimal | :? single | :? double -> $"({column})::numeric {it.Op} {names}" | :? decimal | :? single | :? double -> $"({it.PgSqlPath})::numeric {it.Op} {names}"
| _ -> $"{column} {it.Op} {names}" | _ -> $"{it.PgSqlPath} {it.Op} {names}"
| _ -> | _ -> $"{it.PgSqlPath} {it.Op} {name.Derive it.ParameterName}")
let p = defaultArg it.ParameterName (nameField ())
$"{column} {it.Op} {p}")
|> String.concat (match howMatched with Any -> " OR " | All -> " AND ") |> String.concat (match howMatched with Any -> " OR " | All -> " AND ")
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
let WhereByFields(fields: Field seq, howMatched) =
whereByFields (List.ofSeq fields) howMatched
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">] [<CompiledName "WhereByField">]

View File

@ -31,20 +31,33 @@ module Configuration =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "FSharpWhereByFields">]
let whereByFields fields howMatched =
let name = ParameterName()
fields
|> List.map (fun it ->
match it.Op with
| EX | NEX -> $"{it.SqlitePath} {it.Op}"
| BT ->
let p = name.Derive it.ParameterName
$"{it.SqlitePath} {it.Op} {p}min AND {p}max"
| _ -> $"{it.SqlitePath} {it.Op} {name.Derive it.ParameterName}")
|> String.concat (match howMatched with Any -> " OR " | All -> " AND ")
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
let WhereByFields(fields: Field seq, howMatched) =
whereByFields (List.ofSeq fields) howMatched
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">] [<CompiledName "WhereByField">]
let whereByField field paramName = let whereByField field paramName =
let theRest = whereByFields [ { field with ParameterName = Some paramName } ] Any
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 /// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">] [<CompiledName "WhereById">]
let whereById paramName = let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName whereByFields [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] Any
/// Data definition /// Data definition
module Definition = module Definition =

View File

@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -24,11 +24,11 @@ public static class CommonCSharpTests
/// Unit tests /// Unit tests
/// </summary> /// </summary>
[Tests] [Tests]
public static readonly Test Unit = TestList("Common.C# Unit", new[] public static readonly Test Unit = TestList("Common.C# Unit",
{ [
TestSequenced( TestSequenced(
TestList("Configuration", new[] TestList("Configuration",
{ [
TestCase("UseSerializer succeeds", () => TestCase("UseSerializer succeeds", () =>
{ {
try try
@ -70,9 +70,9 @@ public static class CommonCSharpTests
Configuration.UseIdField("Id"); Configuration.UseIdField("Id");
} }
}) })
})), ])),
TestList("Op", new[] TestList("Op",
{ [
TestCase("EQ succeeds", () => TestCase("EQ succeeds", () =>
{ {
Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct");
@ -109,9 +109,9 @@ public static class CommonCSharpTests
{ {
Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct");
}) })
}), ]),
TestList("Field", new[] TestList("Field",
{ [
TestCase("EQ succeeds", () => TestCase("EQ succeeds", () =>
{ {
var field = Field.EQ("Test", 14); var field = Field.EQ("Test", 14);
@ -159,7 +159,7 @@ public static class CommonCSharpTests
var field = Field.BT("Age", 18, 49); var field = Field.BT("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect"); Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Op, Op.BT, "Operator incorrect"); Expect.equal(field.Op, Op.BT, "Operator incorrect");
Expect.equal(((FSharpList<object>)field.Value).ToArray(), new object[] { 18, 49 }, "Value incorrect"); Expect.equal(((FSharpList<object>)field.Value).ToArray(), [18, 49], "Value incorrect");
}), }),
TestCase("EX succeeds", () => TestCase("EX succeeds", () =>
{ {
@ -172,25 +172,83 @@ public static class CommonCSharpTests
var field = Field.NEX("Rad"); var field = Field.NEX("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect"); Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.equal(field.Op, Op.NEX, "Operator incorrect"); Expect.equal(field.Op, Op.NEX, "Operator incorrect");
}) }),
}), TestCase("WithParameterName succeeds", () =>
TestList("Query", new[] {
{ var field = Field.EQ("Bob", "Tom").WithParameterName("@name");
Expect.isSome(field.ParameterName, "The parameter name should have been filled");
Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect");
}),
TestCase("WithQualifier succeeds", () =>
{
var field = Field.EQ("Bill", "Matt").WithQualifier("joe");
Expect.isSome(field.Qualifier, "The table qualifier should have been filled");
Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect");
}),
TestList("PgSqlPath",
[
TestCase("succeeds for a single field with no qualifier", () =>
{
var field = Field.GE("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.PgSqlPath, "The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a single field with a qualifier", () =>
{
var field = Field.LT("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.PgSqlPath, "The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a nested field with no qualifier", () =>
{
var field = Field.EQ("My.Nested.Field", "howdy");
Expect.equal("data#>>'{My,Nested,Field}'", field.PgSqlPath, "The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a nested field with a qualifier", () =>
{
var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data#>>'{Nest,Away}'", field.PgSqlPath, "The PostgreSQL path is incorrect");
})
]),
TestList("SqlitePath",
[
TestCase("succeeds for a single field with no qualifier", () =>
{
var field = Field.GE("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.SqlitePath, "The SQLite path is incorrect");
}),
TestCase("succeeds for a single field with a qualifier", () =>
{
var field = Field.LT("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.SqlitePath, "The SQLite path is incorrect");
}),
TestCase("succeeds for a nested field with no qualifier", () =>
{
var field = Field.EQ("My.Nested.Field", "howdy");
Expect.equal("data->>'My'->>'Nested'->>'Field'", field.SqlitePath, "The SQLite path is incorrect");
}),
TestCase("succeeds for a nested field with a qualifier", () =>
{
var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data->>'Nest'->>'Away'", field.SqlitePath, "The SQLite path is incorrect");
})
])
]),
TestList("Query",
[
TestCase("SelectFromTable succeeds", () => TestCase("SelectFromTable succeeds", () =>
{ {
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");
}), }),
TestList("Definition", new[] TestList("Definition",
{ [
TestCase("EnsureTableFor succeeds", () => TestCase("EnsureTableFor succeeds", () =>
{ {
Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"),
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
"CREATE TABLE statement not constructed correctly"); "CREATE TABLE statement not constructed correctly");
}), }),
TestList("EnsureKey", new[] TestList("EnsureKey",
{ [
TestCase("succeeds when a schema is present", () => TestCase("succeeds when a schema is present", () =>
{ {
Expect.equal(Query.Definition.EnsureKey("test.table"), Expect.equal(Query.Definition.EnsureKey("test.table"),
@ -203,7 +261,7 @@ public static class CommonCSharpTests
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))", "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))",
"CREATE INDEX for key statement without schema not constructed correctly"); "CREATE INDEX for key statement without schema not constructed correctly");
}) })
}), ]),
TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => TestCase("EnsureIndexOn succeeds for multiple fields and directions", () =>
{ {
Expect.equal( Expect.equal(
@ -213,7 +271,7 @@ public static class CommonCSharpTests
+ "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)",
"CREATE INDEX for multiple field statement incorrect"); "CREATE INDEX for multiple field statement incorrect");
}) })
}), ]),
TestCase("Insert succeeds", () => TestCase("Insert succeeds", () =>
{ {
Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct");
@ -224,6 +282,6 @@ public static class CommonCSharpTests
"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT 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");
}) })
}) ])
}); ]);
} }

View File

@ -16,10 +16,10 @@ public static class PostgresCSharpTests
/// <summary> /// <summary>
/// Tests which do not hit the database /// Tests which do not hit the database
/// </summary> /// </summary>
private static readonly Test Unit = TestList("Unit", new[] private static readonly Test Unit = TestList("Unit",
{ [
TestList("Parameters", new[] TestList("Parameters",
{ [
TestCase("Id succeeds", () => TestCase("Id succeeds", () =>
{ {
var it = Parameters.Id(88); var it = Parameters.Id(88);
@ -32,38 +32,86 @@ public static class PostgresCSharpTests
Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly");
Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect");
}), }),
TestList("AddField", new [] TestList("AddField",
{ [
TestCase("succeeds when a parameter is added", () => TestCase("succeeds when a parameter is added", () =>
{ {
var it = Parameters var it = Parameters.AddField("@field", Field.EQ("it", "242"), []).ToList();
.AddField("@field", Field.EQ("it", "242"), Enumerable.Empty<Tuple<string, SqlValue>>())
.ToList();
Expect.hasLength(it, 1, "There should have been a parameter added"); Expect.hasLength(it, 1, "There should have been a parameter added");
Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly"); Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly");
Expect.isTrue(it[0].Item2.IsParameter, "Field parameter value incorrect"); Expect.isTrue(it[0].Item2.IsParameter, "Field parameter value incorrect");
}), }),
TestCase("succeeds when a parameter is not added", () => TestCase("succeeds when a parameter is not added", () =>
{ {
var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>()); var it = Parameters.AddField("@it", Field.EX("It"), []);
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", () => TestCase("succeeds when two parameters are added", () =>
{ {
var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), []).ToList();
Enumerable.Empty<Tuple<string, SqlValue>>()).ToList();
Expect.hasLength(it, 2, "There should have been 2 parameters added"); Expect.hasLength(it, 2, "There should have been 2 parameters added");
Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct"); Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct");
Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect"); Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect");
Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct"); Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct");
Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect");
}) })
}) ])
}), ]),
TestList("Query", new[] TestList("Query",
{ [
TestList("WhereByField", new[] TestList("WhereByFields",
{ [
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.GT("theField", 0).WithParameterName("@test")],
FieldMatch.Any),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
Expect.equal(Postgres.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed with numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")],
FieldMatch.All),
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed with non-numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("field0", "a", "b").WithParameterName("@alpha")],
FieldMatch.Any),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")],
FieldMatch.All),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)],
FieldMatch.Any),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")],
FieldMatch.All),
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
})
]),
TestList("WhereByField",
[
TestCase("succeeds when a logical operator is passed", () => TestCase("succeeds when a logical operator is passed", () =>
{ {
Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"), Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"),
@ -84,13 +132,13 @@ public static class PostgresCSharpTests
Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"), Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
}) })
}), ]),
TestCase("WhereById succeeds", () => TestCase("WhereById succeeds", () =>
{ {
Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
}), }),
TestList("Definition", new[] TestList("Definition",
{ [
TestCase("EnsureTable succeeds", () => TestCase("EnsureTable succeeds", () =>
{ {
Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName),
@ -112,7 +160,7 @@ public static class PostgresCSharpTests
PostgresDb.TableName), PostgresDb.TableName),
"CREATE INDEX statement not constructed correctly"); "CREATE INDEX statement not constructed correctly");
}) })
}), ]),
TestCase("Update succeeds", () => TestCase("Update succeeds", () =>
{ {
Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
@ -128,8 +176,8 @@ public static class PostgresCSharpTests
Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath",
"WHERE clause not correct"); "WHERE clause not correct");
}), }),
TestList("Count", new[] TestList("Count",
{ [
TestCase("All succeeds", () => TestCase("All succeeds", () =>
{ {
Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName),
@ -153,9 +201,9 @@ public static class PostgresCSharpTests
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"JSON Path match count query not correct"); "JSON Path match count query not correct");
}) })
}), ]),
TestList("Exists", new[] TestList("Exists",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName),
@ -180,9 +228,9 @@ public static class PostgresCSharpTests
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it", $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it",
"JSON Path match existence query not correct"); "JSON Path match existence query not correct");
}) })
}), ]),
TestList("Find", new[] TestList("Find",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName),
@ -207,9 +255,9 @@ public static class PostgresCSharpTests
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"SELECT by JSON Path match query not correct"); "SELECT by JSON Path match query not correct");
}) })
}), ]),
TestList("Patch", new[] TestList("Patch",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName),
@ -234,9 +282,9 @@ public static class PostgresCSharpTests
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath",
"UPDATE partial by JSON Path statement not correct"); "UPDATE partial by JSON Path statement not correct");
}) })
}), ]),
TestList("RemoveFields", new[] TestList("RemoveFields",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName),
@ -261,9 +309,9 @@ public static class PostgresCSharpTests
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath", $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath",
"Remove field by JSON path query not correct"); "Remove field by JSON path query not correct");
}) })
}), ]),
TestList("Delete", new[] TestList("Delete",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName),
@ -288,18 +336,18 @@ public static class PostgresCSharpTests
$"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"DELETE by JSON Path match query not correct"); "DELETE by JSON Path match query not correct");
}) })
}) ])
}) ])
}); ]);
private static readonly List<JsonDocument> TestDocuments = new() private static readonly List<JsonDocument> TestDocuments =
{ [
new() { Id = "one", Value = "FIRST!", NumValue = 0 }, new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 }, new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } }, new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 } new() { Id = "five", Value = "purple", NumValue = 18 }
}; ];
/// <summary> /// <summary>
/// Add the test documents to the database /// Add the test documents to the database
@ -312,10 +360,10 @@ public static class PostgresCSharpTests
/// <summary> /// <summary>
/// Integration tests for the PostgreSQL library /// Integration tests for the PostgreSQL library
/// </summary> /// </summary>
private static readonly Test Integration = TestList("Integration", new[] private static readonly Test Integration = TestList("Integration",
{ [
TestList("Configuration", new[] TestList("Configuration",
{ [
TestCase("UseDataSource disposes existing source", () => TestCase("UseDataSource disposes existing source", () =>
{ {
using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value);
@ -343,11 +391,11 @@ public static class PostgresCSharpTests
Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()),
"Data source should have been the same"); "Data source should have been the same");
}) })
}), ]),
TestList("Custom", new[] TestList("Custom",
{ [
TestList("List", new[] TestList("List",
{ [
TestCase("succeeds when data is found", async () => TestCase("succeeds when data is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -368,9 +416,9 @@ public static class PostgresCSharpTests
Results.FromData<JsonDocument>); Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("Single", new[] TestList("Single",
{ [
TestCase("succeeds when a row is found", async () => TestCase("succeeds when a row is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -390,9 +438,9 @@ public static class PostgresCSharpTests
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>); new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("NonQuery", new[] TestList("NonQuery",
{ [
TestCase("succeeds when operating on data", async () => TestCase("succeeds when operating on data", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -414,7 +462,7 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName); var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); Expect.equal(remaining, 5, "There should be 5 documents remaining in the table");
}) })
}), ]),
TestCase("Scalar succeeds", async () => TestCase("Scalar succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -422,65 +470,71 @@ public static class PostgresCSharpTests
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value")); var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value"));
Expect.equal(nbr, 5, "The query should have returned the number 5"); Expect.equal(nbr, 5, "The query should have returned the number 5");
}) })
}), ]),
TestList("Definition", new[] TestList("Definition",
{ [
TestCase("EnsureTable succeeds", async () => TestCase("EnsureTable succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
var tableExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None,
Results.ToExists);
var keyExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None,
Results.ToExists);
var exists = await tableExists(); var exists = await TableExists();
var alsoExists = await keyExists(); var alsoExists = await KeyExists();
Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already");
await Definition.EnsureTable("ensured"); await Definition.EnsureTable("ensured");
exists = await tableExists(); exists = await TableExists();
alsoExists = await keyExists(); alsoExists = await KeyExists();
Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist"); Expect.isTrue(alsoExists, "The key index should now exist");
return;
Task<bool> TableExists() => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None,
Results.ToExists);
Task<bool> KeyExists() => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None,
Results.ToExists);
}), }),
TestCase("EnsureDocumentIndex succeeds", async () => TestCase("EnsureDocumentIndex succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured"); await Definition.EnsureTable("ensured");
await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized);
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it",
Parameters.None, Results.ToExists);
}), }),
TestCase("EnsureFieldIndex succeeds", async () => TestCase("EnsureFieldIndex succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None,
Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured"); await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None,
Results.ToExists);
}) })
}), ]),
TestList("Document", new[] TestList("Document",
{ [
TestList("Insert", new[] TestList("Insert",
{ [
TestCase("succeeds", async () => TestCase("succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -506,9 +560,9 @@ public static class PostgresCSharpTests
// This is what should have happened // This is what should have happened
} }
}) })
}), ]),
TestList("Save", new[] TestList("Save",
{ [
TestCase("succeeds when a document is inserted", async () => TestCase("succeeds when a document is inserted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -537,10 +591,10 @@ public static class PostgresCSharpTests
Expect.equal(after.Id, "test", "The document is not correct"); Expect.equal(after.Id, "test", "The document is not correct");
Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct");
}) })
}) ])
}), ]),
TestList("Count", new[] TestList("Count",
{ [
TestCase("All succeeds", async () => TestCase("All succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -581,11 +635,11 @@ public static class PostgresCSharpTests
var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents"); Expect.equal(theCount, 3, "There should have been 3 matching documents");
}) })
}), ]),
TestList("Exists", new[] TestList("Exists",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document exists", async () => TestCase("succeeds when a document exists", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -602,9 +656,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ById(PostgresDb.TableName, "seven"); var exists = await Exists.ById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document"); Expect.isFalse(exists, "There should not have been an existing document");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -621,9 +675,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six"));
Expect.isFalse(exists, "There should not have been existing documents"); Expect.isFalse(exists, "There should not have been existing documents");
}) })
}), ]),
TestList("ByContains", new[] TestList("ByContains",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -640,8 +694,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}), ]),
TestList("ByJsonPath", new[] { TestList("ByJsonPath",
[
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -658,12 +713,12 @@ public static class PostgresCSharpTests
var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}) ])
}), ]),
TestList("Find", new[] TestList("Find",
{ [
TestList("All", new[] TestList("All",
{ [
TestCase("succeeds when there is data", async () => TestCase("succeeds when there is data", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -681,9 +736,9 @@ public static class PostgresCSharpTests
var results = await Find.All<SubDocument>(PostgresDb.TableName); var results = await Find.All<SubDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned"); Expect.isEmpty(results, "There should have been no documents returned");
}) })
}), ]),
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -701,9 +756,9 @@ public static class PostgresCSharpTests
var doc = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven"); var doc = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -720,9 +775,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "mauve")); var docs = await Find.ByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("ByContains", new[] TestList("ByContains",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -740,9 +795,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" }); var docs = await Find.ByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("ByJsonPath", new[] TestList("ByJsonPath",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -759,9 +814,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); var docs = await Find.ByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FirstByField", new[] TestList("FirstByField",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -788,9 +843,9 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "absent")); var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FirstByContains", new[] TestList("FirstByContains",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -818,9 +873,9 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" }); var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" });
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FirstByJsonPath", new[] TestList("FirstByJsonPath",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -849,12 +904,12 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}) ])
}), ]),
TestList("Update", new[] TestList("Update",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -882,9 +937,9 @@ public static class PostgresCSharpTests
await Update.ById(PostgresDb.TableName, "test", await Update.ById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
}) })
}), ]),
TestList("ByFunc", new[] TestList("ByFunc",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -910,12 +965,12 @@ public static class PostgresCSharpTests
await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, await Update.ByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
}) })
}) ])
}), ]),
TestList("Patch", new[] TestList("Patch",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -936,9 +991,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" });
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -958,9 +1013,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
}) })
}), ]),
TestList("ByContains", new[] TestList("ByContains",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -980,9 +1035,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
}) })
}), ]),
TestList("ByJsonPath", new[] TestList("ByJsonPath",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1002,12 +1057,12 @@ public static class PostgresCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
}) })
}) ])
}), ]),
TestList("RemoveFields", new[] TestList("RemoveFields",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1045,9 +1100,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" });
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1087,9 +1142,9 @@ public static class PostgresCSharpTests
await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"),
new[] { "Value" }); new[] { "Value" });
}) })
}), ]),
TestList("ByContains", new[] TestList("ByContains",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1129,9 +1184,9 @@ public static class PostgresCSharpTests
await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" },
new[] { "Value" }); new[] { "Value" });
}) })
}), ]),
TestList("ByJsonPath", new[] TestList("ByJsonPath",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1171,12 +1226,12 @@ public static class PostgresCSharpTests
await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")",
new[] { "Value" }); new[] { "Value" });
}) })
}) ])
}), ]),
TestList("Delete", new[] TestList("Delete",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is deleted", async () => TestCase("succeeds when a document is deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1195,9 +1250,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName); var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1216,9 +1271,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName); var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("ByContains", new[] TestList("ByContains",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1237,9 +1292,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName); var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("ByJsonPath", new[] TestList("ByJsonPath",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -1258,13 +1313,13 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName); var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}) ])
}) ])
}); ]);
/// <summary> /// <summary>
/// All Postgres C# tests /// All Postgres C# tests
/// </summary> /// </summary>
[Tests] [Tests]
public static readonly Test All = TestList("Postgres.C#", new[] { Unit, TestSequenced(Integration) }); public static readonly Test All = TestList("Postgres.C#", [Unit, TestSequenced(Integration)]);
} }

View File

@ -1,5 +1,4 @@
using System.Text.Json; using Expecto.CSharp;
using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.FSharp.Core; using Microsoft.FSharp.Core;
@ -17,12 +16,56 @@ public static class SqliteCSharpTests
/// <summary> /// <summary>
/// Unit tests for the SQLite library /// Unit tests for the SQLite library
/// </summary> /// </summary>
private static readonly Test Unit = TestList("Unit", new[] private static readonly Test Unit = TestList("Unit",
{ [
TestList("Query", new[] TestList("Query",
{ [
TestList("WhereByField", new[] TestList("WhereByFields",
{ [
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.GT("theField", 0).WithParameterName("@test")],
FieldMatch.Any),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")],
FieldMatch.All),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")],
FieldMatch.All),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)],
FieldMatch.Any),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")],
FieldMatch.All),
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
})
]),
TestList("WhereByField",
[
TestCase("succeeds when a logical operator is passed", () => TestCase("succeeds when a logical operator is passed", () =>
{ {
Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"), Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"),
@ -38,7 +81,7 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}) })
}), ]),
TestCase("WhereById succeeds", () => TestCase("WhereById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
@ -53,8 +96,8 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
"UPDATE full statement not correct"); "UPDATE full statement not correct");
}), }),
TestList("Count", new[] TestList("Count",
{ [
TestCase("All succeeds", () => TestCase("All succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl",
@ -66,9 +109,9 @@ public static class SqliteCSharpTests
"SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field", "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field",
"JSON field text comparison count query not correct"); "JSON field text comparison count query not correct");
}) })
}), ]),
TestList("Exists", new[] TestList("Exists",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Exists.ById("tbl"), Expect.equal(Sqlite.Query.Exists.ById("tbl"),
@ -81,9 +124,9 @@ public static class SqliteCSharpTests
"SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it", "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it",
"JSON field text comparison exists query not correct"); "JSON field text comparison exists query not correct");
}) })
}), ]),
TestList("Find", new[] TestList("Find",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id",
@ -95,9 +138,9 @@ public static class SqliteCSharpTests
"SELECT data FROM tbl WHERE data->>'Golf' >= @field", "SELECT data FROM tbl WHERE data->>'Golf' >= @field",
"SELECT by JSON comparison query not correct"); "SELECT by JSON comparison query not correct");
}) })
}), ]),
TestList("Patch", new[] TestList("Patch",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Patch.ById("tbl"), Expect.equal(Sqlite.Query.Patch.ById("tbl"),
@ -110,9 +153,9 @@ public static class SqliteCSharpTests
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field", "UPDATE 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");
}) })
}), ]),
TestList("RemoveFields", new[] TestList("RemoveFields",
{ [
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") }),
@ -126,9 +169,9 @@ public static class SqliteCSharpTests
"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[] TestList("Delete",
{ [
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id",
@ -139,10 +182,10 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.Delete.ByField("tbl", Field.NEX("gone")), 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"); "DELETE FROM tbl WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct");
}) })
}) ])
}), ]),
TestList("Parameters", new[] TestList("Parameters",
{ [
TestCase("Id succeeds", () => TestCase("Id succeeds", () =>
{ {
var theParam = Parameters.Id(7); var theParam = Parameters.Id(7);
@ -157,8 +200,7 @@ public static class SqliteCSharpTests
}), }),
TestCase("AddField succeeds when adding a parameter", () => TestCase("AddField succeeds when adding a parameter", () =>
{ {
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), Enumerable.Empty<SqliteParameter>()) var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList();
.ToList();
Expect.hasLength(paramList, 1, "There should have been a parameter added"); Expect.hasLength(paramList, 1, "There should have been a parameter added");
var theParam = paramList[0]; var theParam = paramList[0];
Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect");
@ -166,25 +208,25 @@ public static class SqliteCSharpTests
}), }),
TestCase("AddField succeeds when not adding a parameter", () => TestCase("AddField succeeds when not adding a parameter", () =>
{ {
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), Enumerable.Empty<SqliteParameter>()); var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []);
Expect.isEmpty(paramSeq, "There should not have been any parameters added"); Expect.isEmpty(paramSeq, "There should not have been any parameters added");
}), }),
TestCase("None succeeds", () => TestCase("None succeeds", () =>
{ {
Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); Expect.isEmpty(Parameters.None, "The parameter list should have been empty");
}) })
}) ])
// Results are exhaustively executed in the context of other tests // Results are exhaustively executed in the context of other tests
}); ]);
private static readonly List<JsonDocument> TestDocuments = new() private static readonly List<JsonDocument> TestDocuments =
{ [
new() { Id = "one", Value = "FIRST!", NumValue = 0 }, new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 }, new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } }, new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 } new() { Id = "five", Value = "purple", NumValue = 18 }
}; ];
/// <summary> /// <summary>
/// Add the test documents to the database /// Add the test documents to the database
@ -194,8 +236,8 @@ public static class SqliteCSharpTests
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
} }
private static readonly Test Integration = TestList("Integration", new[] private static readonly Test Integration = TestList("Integration",
{ [
TestCase("Configuration.UseConnectionString succeeds", () => TestCase("Configuration.UseConnectionString succeeds", () =>
{ {
try try
@ -209,10 +251,10 @@ public static class SqliteCSharpTests
Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); Sqlite.Configuration.UseConnectionString("Data Source=:memory:");
} }
}), }),
TestList("Custom", new[] TestList("Custom",
{ [
TestList("Single", new[] TestList("Single",
{ [
TestCase("succeeds when a row is found", async () => TestCase("succeeds when a row is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -232,9 +274,9 @@ public static class SqliteCSharpTests
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>); new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("List", new[] TestList("List",
{ [
TestCase("succeeds when data is found", async () => TestCase("succeeds when data is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -254,9 +296,9 @@ public static class SqliteCSharpTests
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>); new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("NonQuery", new[] TestList("NonQuery",
{ [
TestCase("succeeds when operating on data", async () => TestCase("succeeds when operating on data", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -278,7 +320,7 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName); var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table");
}) })
}), ]),
TestCase("Scalar succeeds", async () => TestCase("Scalar succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -286,9 +328,9 @@ public static class SqliteCSharpTests
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0));
Expect.equal(nbr, 5, "The query should have returned the number 5"); Expect.equal(nbr, 5, "The query should have returned the number 5");
}) })
}), ]),
TestList("Definition", new[] TestList("Definition",
{ [
TestCase("EnsureTable succeeds", async () => TestCase("EnsureTable succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -316,21 +358,23 @@ public static class SqliteCSharpTests
TestCase("EnsureFieldIndex succeeds", async () => TestCase("EnsureFieldIndex succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
var indexExists = () => Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured"); await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() => Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
}) })
}), ]),
TestList("Document.Insert", new[] TestList("Document.Insert",
{ [
TestCase("succeeds", async () => TestCase("succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -355,9 +399,9 @@ public static class SqliteCSharpTests
// This is what is supposed to happen // This is what is supposed to happen
} }
}) })
}), ]),
TestList("Document.Save", new[] TestList("Document.Save",
{ [
TestCase("succeeds when a document is inserted", async () => TestCase("succeeds when a document is inserted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -388,9 +432,9 @@ public static class SqliteCSharpTests
Expect.equal(after!.Id, "test", "The updated document is not correct"); Expect.equal(after!.Id, "test", "The updated document is not correct");
Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document");
}) })
}), ]),
TestList("Count", new[] TestList("Count",
{ [
TestCase("All succeeds", async () => TestCase("All succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -415,11 +459,11 @@ public static class SqliteCSharpTests
var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple")); var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple"));
Expect.equal(theCount, 1L, "There should have been 1 matching document"); Expect.equal(theCount, 1L, "There should have been 1 matching document");
}) })
}), ]),
TestList("Exists", new[] TestList("Exists",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document exists", async () => TestCase("succeeds when a document exists", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -436,9 +480,9 @@ public static class SqliteCSharpTests
var exists = await Exists.ById(SqliteDb.TableName, "seven"); var exists = await Exists.ById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document"); Expect.isFalse(exists, "There should not have been an existing document");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -455,12 +499,12 @@ public static class SqliteCSharpTests
var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none"));
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}) ])
}), ]),
TestList("Find", new[] TestList("Find",
{ [
TestList("All", new[] TestList("All",
{ [
TestCase("succeeds when there is data", async () => TestCase("succeeds when there is data", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -478,9 +522,9 @@ public static class SqliteCSharpTests
var results = await Find.All<SubDocument>(SqliteDb.TableName); var results = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned"); Expect.isEmpty(results, "There should have been no documents returned");
}) })
}), ]),
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -498,9 +542,9 @@ public static class SqliteCSharpTests
var doc = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "twenty two"); var doc = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "twenty two");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -517,9 +561,9 @@ public static class SqliteCSharpTests
var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve")); var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FirstByField", new[] TestList("FirstByField",
{ [
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -546,12 +590,12 @@ public static class SqliteCSharpTests
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent")); var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}) ])
}), ]),
TestList("Update", new[] TestList("Update",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -577,9 +621,9 @@ public static class SqliteCSharpTests
await Update.ById(SqliteDb.TableName, "test", await Update.ById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
}) })
}), ]),
TestList("ByFunc", new[] TestList("ByFunc",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -605,12 +649,12 @@ public static class SqliteCSharpTests
await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, await Update.ByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
}) })
}), ]),
}), ]),
TestList("Patch", new[] TestList("Patch",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -632,9 +676,9 @@ public static class SqliteCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" });
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -654,12 +698,12 @@ public static class SqliteCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
}) })
}) ])
}), ]),
TestList("RemoveFields", new[] TestList("RemoveFields",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when fields are removed", async () => TestCase("succeeds when fields are removed", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -686,9 +730,9 @@ public static class SqliteCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" });
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when a field is removed", async () => TestCase("succeeds when a field is removed", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -714,12 +758,12 @@ public static class SqliteCSharpTests
// This not raising an exception is the test // This not raising an exception is the test
await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" });
}) })
}) ])
}), ]),
TestList("Delete", new[] TestList("Delete",
{ [
TestList("ById", new[] TestList("ById",
{ [
TestCase("succeeds when a document is deleted", async () => TestCase("succeeds when a document is deleted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -738,9 +782,9 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName); var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("ByField", new[] TestList("ByField",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -759,14 +803,14 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName); var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
}) })
}) ])
}), ]),
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
}); ]);
/// <summary> /// <summary>
/// All tests for SQLite C# functions and methods /// All tests for SQLite C# functions and methods
/// </summary> /// </summary>
[Tests] [Tests]
public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) }); public static readonly Test All = TestList("Sqlite.C#", [Unit, TestSequenced(Integration)]);
} }

View File

@ -119,6 +119,56 @@ let all =
Expect.isSome field.Qualifier "The table qualifier should have been filled" Expect.isSome field.Qualifier "The table qualifier should have been filled"
Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect" Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
} }
testList "PgSqlPath" [
test "succeeds for a single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
]
testList "SqlitePath" [
test "succeeds for a single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data->>'My'->>'Nested'->>'Field'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->>'Nest'->>'Away'" field.SqlitePath "The SQLite path is incorrect"
}
]
]
testList "ParameterName" [
test "Derive succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "Derive succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
] ]
testList "Query" [ testList "Query" [
test "selectFromTable succeeds" { test "selectFromTable succeeds" {

View File

@ -58,6 +58,50 @@ let unitTests =
} }
] ]
testList "Query" [ testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ] Any)
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields [ Field.NEX "thatField" ] Any)
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed with numeric values" {
Expect.equal
(Query.whereByFields [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ] All)
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed with non-numeric values" {
Expect.equal
(Query.whereByFields [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ] Any)
"data->>'field0' BETWEEN @alphamin AND @alphamax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All)
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields [ Field.NEX "thatField"; Field.GE "thisField" 18 ] Any)
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ] All)
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
]
testList "whereByField" [ testList "whereByField" [
test "succeeds when a logical operator is passed" { test "succeeds when a logical operator is passed" {
Expect.equal Expect.equal

View File

@ -12,6 +12,44 @@ open Types
let unitTests = let unitTests =
testList "Unit" [ testList "Unit" [
testList "Query" [ testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ] Any)
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields [ Field.NEX "thatField" ] Any)
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed" {
Expect.equal
(Query.whereByFields [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ] All)
"data->>'aField' BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ] All)
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields [ Field.NEX "thatField"; Field.GE "thisField" 18 ] Any)
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ] All)
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
]
testList "whereByField" [ testList "whereByField" [
test "succeeds when a logical operator is passed" { test "succeeds when a logical operator is passed" {
Expect.equal Expect.equal