Version 4 #6

Merged
danieljsummers merged 30 commits from version-four into main 2024-08-19 23:30:39 +00:00
10 changed files with 695 additions and 373 deletions
Showing only changes of commit e2232e91bb - Show all commits

View File

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

View File

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

View File

@ -31,20 +31,33 @@ module Configuration =
[<RequireQualifiedAccess>]
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
[<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}"
whereByFields [ { field with ParameterName = Some paramName } ] Any
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
whereByFields [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] Any
/// Data definition
module Definition =

View File

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

View File

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

View File

@ -16,10 +16,10 @@ public static class PostgresCSharpTests
/// <summary>
/// Tests which do not hit the database
/// </summary>
private static readonly Test Unit = TestList("Unit", new[]
{
TestList("Parameters", new[]
{
private static readonly Test Unit = TestList("Unit",
[
TestList("Parameters",
[
TestCase("Id succeeds", () =>
{
var it = Parameters.Id(88);
@ -32,38 +32,86 @@ public static class PostgresCSharpTests
Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly");
Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect");
}),
TestList("AddField", new []
{
TestList("AddField",
[
TestCase("succeeds when a parameter is added", () =>
{
var it = Parameters
.AddField("@field", Field.EQ("it", "242"), Enumerable.Empty<Tuple<string, SqlValue>>())
.ToList();
var it = Parameters.AddField("@field", Field.EQ("it", "242"), []).ToList();
Expect.hasLength(it, 1, "There should have been a parameter added");
Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly");
Expect.isTrue(it[0].Item2.IsParameter, "Field parameter value incorrect");
}),
TestCase("succeeds when a parameter is not added", () =>
{
var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>());
var it = Parameters.AddField("@it", Field.EX("It"), []);
Expect.isEmpty(it, "There should not have been any parameters added");
}),
TestCase("succeeds when two parameters are added", () =>
{
var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"),
Enumerable.Empty<Tuple<string, SqlValue>>()).ToList();
var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), []).ToList();
Expect.hasLength(it, 2, "There should have been 2 parameters added");
Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct");
Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect");
Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct");
Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect");
})
})
])
]),
TestList("Query",
[
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");
}),
TestList("Query", new[]
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
TestList("WhereByField", new[]
Expect.equal(Postgres.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed with numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")],
FieldMatch.All),
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed with non-numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("field0", "a", "b").WithParameterName("@alpha")],
FieldMatch.Any),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")],
FieldMatch.All),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)],
FieldMatch.Any),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")],
FieldMatch.All),
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
})
]),
TestList("WhereByField",
[
TestCase("succeeds when a logical operator is passed", () =>
{
Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"),
@ -84,13 +132,13 @@ public static class PostgresCSharpTests
Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
})
}),
]),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
}),
TestList("Definition", new[]
{
TestList("Definition",
[
TestCase("EnsureTable succeeds", () =>
{
Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName),
@ -112,7 +160,7 @@ public static class PostgresCSharpTests
PostgresDb.TableName),
"CREATE INDEX statement not constructed correctly");
})
}),
]),
TestCase("Update succeeds", () =>
{
Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
@ -128,8 +176,8 @@ public static class PostgresCSharpTests
Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath",
"WHERE clause not correct");
}),
TestList("Count", new[]
{
TestList("Count",
[
TestCase("All succeeds", () =>
{
Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName),
@ -153,9 +201,9 @@ public static class PostgresCSharpTests
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"JSON Path match count query not correct");
})
}),
TestList("Exists", new[]
{
]),
TestList("Exists",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName),
@ -180,9 +228,9 @@ public static class PostgresCSharpTests
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it",
"JSON Path match existence query not correct");
})
}),
TestList("Find", new[]
{
]),
TestList("Find",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName),
@ -207,9 +255,9 @@ public static class PostgresCSharpTests
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"SELECT by JSON Path match query not correct");
})
}),
TestList("Patch", new[]
{
]),
TestList("Patch",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName),
@ -234,9 +282,9 @@ public static class PostgresCSharpTests
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath",
"UPDATE partial by JSON Path statement not correct");
})
}),
TestList("RemoveFields", new[]
{
]),
TestList("RemoveFields",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName),
@ -261,9 +309,9 @@ public static class PostgresCSharpTests
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath",
"Remove field by JSON path query not correct");
})
}),
TestList("Delete", new[]
{
]),
TestList("Delete",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName),
@ -288,18 +336,18 @@ public static class PostgresCSharpTests
$"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"DELETE by JSON Path match query not correct");
})
})
})
});
])
])
]);
private static readonly List<JsonDocument> TestDocuments = new()
{
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
};
];
/// <summary>
/// Add the test documents to the database
@ -312,10 +360,10 @@ public static class PostgresCSharpTests
/// <summary>
/// Integration tests for the PostgreSQL library
/// </summary>
private static readonly Test Integration = TestList("Integration", new[]
{
TestList("Configuration", new[]
{
private static readonly Test Integration = TestList("Integration",
[
TestList("Configuration",
[
TestCase("UseDataSource disposes existing source", () =>
{
using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value);
@ -343,11 +391,11 @@ public static class PostgresCSharpTests
Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()),
"Data source should have been the same");
})
}),
TestList("Custom", new[]
{
TestList("List", new[]
{
]),
TestList("Custom",
[
TestList("List",
[
TestCase("succeeds when data is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -368,9 +416,9 @@ public static class PostgresCSharpTests
Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("Single", new[]
{
]),
TestList("Single",
[
TestCase("succeeds when a row is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -390,9 +438,9 @@ public static class PostgresCSharpTests
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("NonQuery", new[]
{
]),
TestList("NonQuery",
[
TestCase("succeeds when operating on data", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -414,7 +462,7 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should be 5 documents remaining in the table");
})
}),
]),
TestCase("Scalar succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -422,65 +470,71 @@ public static class PostgresCSharpTests
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value"));
Expect.equal(nbr, 5, "The query should have returned the number 5");
})
}),
TestList("Definition", new[]
{
]),
TestList("Definition",
[
TestCase("EnsureTable succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var tableExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None,
Results.ToExists);
var keyExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None,
Results.ToExists);
var exists = await tableExists();
var alsoExists = await keyExists();
var exists = await TableExists();
var alsoExists = await KeyExists();
Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already");
await Definition.EnsureTable("ensured");
exists = await tableExists();
alsoExists = await keyExists();
exists = await TableExists();
alsoExists = await KeyExists();
Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist");
return;
Task<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 () =>
{
await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists();
var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized);
exists = await indexExists();
exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist");
return;
Task<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 () =>
{
await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None,
Results.ToExists);
var exists = await indexExists();
var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist");
return;
Task<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("Insert", new[]
{
]),
TestList("Document",
[
TestList("Insert",
[
TestCase("succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -506,9 +560,9 @@ public static class PostgresCSharpTests
// This is what should have happened
}
})
}),
TestList("Save", new[]
{
]),
TestList("Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -537,10 +591,10 @@ public static class PostgresCSharpTests
Expect.equal(after.Id, "test", "The document is not correct");
Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct");
})
})
}),
TestList("Count", new[]
{
])
]),
TestList("Count",
[
TestCase("All succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -581,11 +635,11 @@ public static class PostgresCSharpTests
var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents");
})
}),
TestList("Exists", new[]
{
TestList("ById", new[]
{
]),
TestList("Exists",
[
TestList("ById",
[
TestCase("succeeds when a document exists", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -602,9 +656,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -621,9 +675,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six"));
Expect.isFalse(exists, "There should not have been existing documents");
})
}),
TestList("ByContains", new[]
{
]),
TestList("ByContains",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -640,8 +694,9 @@ public static class PostgresCSharpTests
var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("ByJsonPath", new[] {
]),
TestList("ByJsonPath",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -658,12 +713,12 @@ public static class PostgresCSharpTests
var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents");
})
})
}),
TestList("Find", new[]
{
TestList("All", new[]
{
])
]),
TestList("Find",
[
TestList("All",
[
TestCase("succeeds when there is data", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -681,9 +736,9 @@ public static class PostgresCSharpTests
var results = await Find.All<SubDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("ById", new[]
{
]),
TestList("ById",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -701,9 +756,9 @@ public static class PostgresCSharpTests
var doc = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -720,9 +775,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("ByContains", new[]
{
]),
TestList("ByContains",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -740,9 +795,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("ByJsonPath", new[]
{
]),
TestList("ByJsonPath",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -759,9 +814,9 @@ public static class PostgresCSharpTests
var docs = await Find.ByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FirstByField", new[]
{
]),
TestList("FirstByField",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -788,9 +843,9 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FirstByContains", new[]
{
]),
TestList("FirstByContains",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -818,9 +873,9 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" });
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FirstByJsonPath", new[]
{
]),
TestList("FirstByJsonPath",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -849,12 +904,12 @@ public static class PostgresCSharpTests
var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned");
})
})
}),
TestList("Update", new[]
{
TestList("ById", new[]
{
])
]),
TestList("Update",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -882,9 +937,9 @@ public static class PostgresCSharpTests
await Update.ById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("ByFunc", new[]
{
]),
TestList("ByFunc",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -910,12 +965,12 @@ public static class PostgresCSharpTests
await Update.ByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
})
}),
TestList("Patch", new[]
{
TestList("ById", new[]
{
])
]),
TestList("Patch",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -936,9 +991,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test
await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -958,9 +1013,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test
await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
})
}),
TestList("ByContains", new[]
{
]),
TestList("ByContains",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -980,9 +1035,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test
await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
})
}),
TestList("ByJsonPath", new[]
{
]),
TestList("ByJsonPath",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1002,12 +1057,12 @@ public static class PostgresCSharpTests
// This not raising an exception is the test
await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
})
})
}),
TestList("RemoveFields", new[]
{
TestList("ById", new[]
{
])
]),
TestList("RemoveFields",
[
TestList("ById",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1045,9 +1100,9 @@ public static class PostgresCSharpTests
// This not raising an exception is the test
await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" });
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1087,9 +1142,9 @@ public static class PostgresCSharpTests
await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"),
new[] { "Value" });
})
}),
TestList("ByContains", new[]
{
]),
TestList("ByContains",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1129,9 +1184,9 @@ public static class PostgresCSharpTests
await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" },
new[] { "Value" });
})
}),
TestList("ByJsonPath", new[]
{
]),
TestList("ByJsonPath",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1171,12 +1226,12 @@ public static class PostgresCSharpTests
await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")",
new[] { "Value" });
})
})
}),
TestList("Delete", new[]
{
TestList("ById", new[]
{
])
]),
TestList("Delete",
[
TestList("ById",
[
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1195,9 +1250,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1216,9 +1271,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByContains", new[]
{
]),
TestList("ByContains",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1237,9 +1292,9 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByJsonPath", new[]
{
]),
TestList("ByJsonPath",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@ -1258,13 +1313,13 @@ public static class PostgresCSharpTests
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
})
})
});
])
])
]);
/// <summary>
/// All Postgres C# tests
/// </summary>
[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 Microsoft.Data.Sqlite;
using Microsoft.FSharp.Core;
@ -17,12 +16,56 @@ public static class SqliteCSharpTests
/// <summary>
/// Unit tests for the SQLite library
/// </summary>
private static readonly Test Unit = TestList("Unit", new[]
private static readonly Test Unit = TestList("Unit",
[
TestList("Query",
[
TestList("WhereByFields",
[
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
TestList("Query", new[]
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", () =>
{
TestList("WhereByField", new[]
Expect.equal(Sqlite.Query.WhereByFields([Field.NEX("thatField")], FieldMatch.Any),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99).WithParameterName("@range")],
FieldMatch.All),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")],
FieldMatch.All),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.NEX("thatField"), Field.GE("thisField", 18)],
FieldMatch.Any),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields([Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")],
FieldMatch.All),
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
})
]),
TestList("WhereByField",
[
TestCase("succeeds when a logical operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"),
@ -38,7 +81,7 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
})
}),
]),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
@ -53,8 +96,8 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
"UPDATE full statement not correct");
}),
TestList("Count", new[]
{
TestList("Count",
[
TestCase("All succeeds", () =>
{
Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl",
@ -66,9 +109,9 @@ public static class SqliteCSharpTests
"SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field",
"JSON field text comparison count query not correct");
})
}),
TestList("Exists", new[]
{
]),
TestList("Exists",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Exists.ById("tbl"),
@ -81,9 +124,9 @@ public static class SqliteCSharpTests
"SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it",
"JSON field text comparison exists query not correct");
})
}),
TestList("Find", new[]
{
]),
TestList("Find",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id",
@ -95,9 +138,9 @@ public static class SqliteCSharpTests
"SELECT data FROM tbl WHERE data->>'Golf' >= @field",
"SELECT by JSON comparison query not correct");
})
}),
TestList("Patch", new[]
{
]),
TestList("Patch",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Patch.ById("tbl"),
@ -110,9 +153,9 @@ public static class SqliteCSharpTests
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field",
"UPDATE partial by JSON comparison query not correct");
})
}),
TestList("RemoveFields", new[]
{
]),
TestList("RemoveFields",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }),
@ -126,9 +169,9 @@ public static class SqliteCSharpTests
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field",
"Remove field by field query not correct");
})
}),
TestList("Delete", new[]
{
]),
TestList("Delete",
[
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id",
@ -139,10 +182,10 @@ public static class SqliteCSharpTests
Expect.equal(Sqlite.Query.Delete.ByField("tbl", Field.NEX("gone")),
"DELETE FROM tbl WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct");
})
})
}),
TestList("Parameters", new[]
{
])
]),
TestList("Parameters",
[
TestCase("Id succeeds", () =>
{
var theParam = Parameters.Id(7);
@ -157,8 +200,7 @@ public static class SqliteCSharpTests
}),
TestCase("AddField succeeds when adding a parameter", () =>
{
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), Enumerable.Empty<SqliteParameter>())
.ToList();
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList();
Expect.hasLength(paramList, 1, "There should have been a parameter added");
var theParam = paramList[0];
Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect");
@ -166,25 +208,25 @@ public static class SqliteCSharpTests
}),
TestCase("AddField succeeds when not adding a parameter", () =>
{
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), Enumerable.Empty<SqliteParameter>());
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []);
Expect.isEmpty(paramSeq, "There should not have been any parameters added");
}),
TestCase("None succeeds", () =>
{
Expect.isEmpty(Parameters.None, "The parameter list should have been empty");
})
})
])
// Results are exhaustively executed in the context of other tests
});
]);
private static readonly List<JsonDocument> TestDocuments = new()
{
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
};
];
/// <summary>
/// Add the test documents to the database
@ -194,8 +236,8 @@ public static class SqliteCSharpTests
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
private static readonly Test Integration = TestList("Integration", new[]
{
private static readonly Test Integration = TestList("Integration",
[
TestCase("Configuration.UseConnectionString succeeds", () =>
{
try
@ -209,10 +251,10 @@ public static class SqliteCSharpTests
Sqlite.Configuration.UseConnectionString("Data Source=:memory:");
}
}),
TestList("Custom", new[]
{
TestList("Single", new[]
{
TestList("Custom",
[
TestList("Single",
[
TestCase("succeeds when a row is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -232,9 +274,9 @@ public static class SqliteCSharpTests
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("List", new[]
{
]),
TestList("List",
[
TestCase("succeeds when data is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -254,9 +296,9 @@ public static class SqliteCSharpTests
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("NonQuery", new[]
{
]),
TestList("NonQuery",
[
TestCase("succeeds when operating on data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -278,7 +320,7 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table");
})
}),
]),
TestCase("Scalar succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -286,9 +328,9 @@ public static class SqliteCSharpTests
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0));
Expect.equal(nbr, 5, "The query should have returned the number 5");
})
}),
TestList("Definition", new[]
{
]),
TestList("Definition",
[
TestCase("EnsureTable succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -316,21 +358,23 @@ public static class SqliteCSharpTests
TestCase("EnsureFieldIndex succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
var indexExists = () => Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists();
var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist");
return;
Task<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 () =>
{
await using var db = await SqliteDb.BuildDb();
@ -355,9 +399,9 @@ public static class SqliteCSharpTests
// This is what is supposed to happen
}
})
}),
TestList("Document.Save", new[]
{
]),
TestList("Document.Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -388,9 +432,9 @@ public static class SqliteCSharpTests
Expect.equal(after!.Id, "test", "The updated document is not correct");
Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document");
})
}),
TestList("Count", new[]
{
]),
TestList("Count",
[
TestCase("All succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -415,11 +459,11 @@ public static class SqliteCSharpTests
var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple"));
Expect.equal(theCount, 1L, "There should have been 1 matching document");
})
}),
TestList("Exists", new[]
{
TestList("ById", new[]
{
]),
TestList("Exists",
[
TestList("ById",
[
TestCase("succeeds when a document exists", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -436,9 +480,9 @@ public static class SqliteCSharpTests
var exists = await Exists.ById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -455,12 +499,12 @@ public static class SqliteCSharpTests
var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none"));
Expect.isFalse(exists, "There should not have been any existing documents");
})
})
}),
TestList("Find", new[]
{
TestList("All", new[]
{
])
]),
TestList("Find",
[
TestList("All",
[
TestCase("succeeds when there is data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -478,9 +522,9 @@ public static class SqliteCSharpTests
var results = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("ById", new[]
{
]),
TestList("ById",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -498,9 +542,9 @@ public static class SqliteCSharpTests
var doc = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "twenty two");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -517,9 +561,9 @@ public static class SqliteCSharpTests
var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FirstByField", new[]
{
]),
TestList("FirstByField",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -546,12 +590,12 @@ public static class SqliteCSharpTests
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned");
})
})
}),
TestList("Update", new[]
{
TestList("ById", new[]
{
])
]),
TestList("Update",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -577,9 +621,9 @@ public static class SqliteCSharpTests
await Update.ById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("ByFunc", new[]
{
]),
TestList("ByFunc",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -605,12 +649,12 @@ public static class SqliteCSharpTests
await Update.ByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
}),
TestList("Patch", new[]
{
TestList("ById", new[]
{
]),
]),
TestList("Patch",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -632,9 +676,9 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -654,12 +698,12 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
})
})
}),
TestList("RemoveFields", new[]
{
TestList("ById", new[]
{
])
]),
TestList("RemoveFields",
[
TestList("ById",
[
TestCase("succeeds when fields are removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -686,9 +730,9 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" });
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when a field is removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -714,12 +758,12 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" });
})
})
}),
TestList("Delete", new[]
{
TestList("ById", new[]
{
])
]),
TestList("Delete",
[
TestList("ById",
[
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -738,9 +782,9 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestList("ByField", new[]
{
]),
TestList("ByField",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@ -759,14 +803,14 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
})
}),
])
]),
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
});
]);
/// <summary>
/// All tests for SQLite C# functions and methods
/// </summary>
[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.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
}
testList "PgSqlPath" [
test "succeeds for a single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
test "succeeds for a nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" field.PgSqlPath "The PostgreSQL path is incorrect"
}
]
testList "SqlitePath" [
test "succeeds for a single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data->>'My'->>'Nested'->>'Field'" field.SqlitePath "The SQLite path is incorrect"
}
test "succeeds for a nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->>'Nest'->>'Away'" field.SqlitePath "The SQLite path is incorrect"
}
]
]
testList "ParameterName" [
test "Derive succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "Derive succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
testList "Query" [
test "selectFromTable succeeds" {

View File

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

View File

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