Version 4 #6

Merged
danieljsummers merged 30 commits from version-four into main 2024-08-19 23:30:39 +00:00
9 changed files with 155 additions and 58 deletions
Showing only changes of commit 0c308c5f33 - Show all commits

View File

@ -126,6 +126,10 @@ type FieldMatch =
/// All fields match (AND) /// All fields match (AND)
| All | All
/// The SQL value implementing each matching strategy
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) /// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
type ParameterName() = type ParameterName() =

View File

@ -46,7 +46,7 @@ module private Helpers =
() ()
} }
/// Create a numerically-typed parameter, or use the given parameter derivation function if non-(numeric or string) /// Create a number or string parameter, or use the given parameter derivation function if non-(numeric or string)
let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) = let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) =
match box value with match box value with
| :? int8 as it -> Sql.int8 it | :? int8 as it -> Sql.int8 it
@ -70,7 +70,7 @@ open BitBadger.Documents
[<AutoOpen>] [<AutoOpen>]
module Parameters = module Parameters =
/// Create an ID parameter (name "@id", key will be treated as a string) /// Create an ID parameter (name "@id")
[<CompiledName "Id">] [<CompiledName "Id">]
let idParam (key: 'TKey) = let idParam (key: 'TKey) =
"@id", parameterFor key (fun it -> Sql.string (string it)) "@id", parameterFor key (fun it -> Sql.string (string it))
@ -134,7 +134,7 @@ module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "WhereByFields">] [<CompiledName "WhereByFields">]
let whereByFields howMatched fields = let whereByFields (howMatched: FieldMatch) fields =
let name = ParameterName() let name = ParameterName()
let isNumeric (it: obj) = let isNumeric (it: obj) =
match it with match it with
@ -154,7 +154,7 @@ module Query =
if isNumeric value then if isNumeric value then
$"({it.Path PostgreSQL})::numeric {it.Op} {param}" $"({it.Path PostgreSQL})::numeric {it.Op} {param}"
else $"{it.Path PostgreSQL} {it.Op} {param}") else $"{it.Path PostgreSQL} {it.Op} {param}")
|> String.concat (match howMatched with Any -> " OR " | All -> " AND ") |> String.concat $" {howMatched} "
/// Create a WHERE clause fragment to implement an ID-based query /// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">] [<CompiledName "WhereById">]

View File

@ -33,7 +33,7 @@ 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 "WhereByFields">]
let whereByFields howMatched fields = let whereByFields (howMatched: FieldMatch) fields =
let name = ParameterName() let name = ParameterName()
fields fields
|> Seq.map (fun it -> |> Seq.map (fun it ->
@ -43,7 +43,7 @@ module Query =
let p = name.Derive it.ParameterName let p = name.Derive it.ParameterName
$"{it.Path SQLite} {it.Op} {p}min AND {p}max" $"{it.Path SQLite} {it.Op} {p}min AND {p}max"
| _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}") | _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}")
|> String.concat (match howMatched with Any -> " OR " | All -> " AND ") |> String.concat $" {howMatched} "
/// Create a WHERE clause fragment to implement an ID-based query /// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">] [<CompiledName "WhereById">]

View File

@ -1,6 +1,7 @@
using Expecto.CSharp; using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.FSharp.Collections; using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Core;
namespace BitBadger.Documents.Tests.CSharp; namespace BitBadger.Documents.Tests.CSharp;
@ -236,6 +237,39 @@ public static class CommonCSharpTests
}) })
]) ])
]), ]),
TestList("FieldMatch.ToString",
[
TestCase("succeeds for Any", () =>
{
Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect");
}),
TestCase("succeeds for All", () =>
{
Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect");
})
]),
TestList("ParameterName.Derive",
[
TestCase("succeeds with existing name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.Some("@taco")), "@taco", "Name should have been @taco");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Counter should not have advanced for named field");
}),
TestCase("Derive succeeds with non-existent name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Anonymous field name should have been returned");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field1",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field2",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field3",
"Counter should have advanced from previous call");
})
]),
TestList("Query", TestList("Query",
[ [
TestCase("StatementWhere succeeds", () => TestCase("StatementWhere succeeds", () =>

View File

@ -156,32 +156,6 @@ public static class PostgresCSharpTests
Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct");
}) })
]), ]),
#pragma warning disable CS0618
TestList("AddField",
[
TestCase("succeeds when a parameter is added", () =>
{
var it = Parameters.AddField("@field", Field.EQ("it", "242"), []).ToList();
Expect.hasLength(it, 1, "There should have been a parameter added");
Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly");
Expect.isTrue(it[0].Item2.IsString, "Field parameter value incorrect");
}),
TestCase("succeeds when a parameter is not added", () =>
{
var it = Parameters.AddField("@it", Field.EX("It"), []);
Expect.isEmpty(it, "There should not have been any parameters added");
}),
TestCase("succeeds when two parameters are added", () =>
{
var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), []).ToList();
Expect.hasLength(it, 2, "There should have been 2 parameters added");
Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct");
Expect.isTrue(it[0].Item2.IsString, "Minimum field parameter value incorrect");
Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct");
Expect.isTrue(it[1].Item2.IsString, "Maximum field parameter value incorrect");
})
]),
#pragma warning restore CS0618
TestList("FieldNames", TestList("FieldNames",
[ [
TestCase("succeeds for one name", () => TestCase("succeeds for one name", () =>
@ -331,6 +305,36 @@ 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");
}),
TestCase("Patch succeeds", () =>
{
Expect.equal(Postgres.Query.Patch(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data || @data", "Patch query not correct");
}),
TestCase("RemoveFields succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data - @name", "Field removal query not correct");
}),
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.ById("test", "14"), "test WHERE data->>'Id' = @id",
"By-ID query not correct");
}),
TestCase("ByFields succeeds", () =>
{
Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]),
"unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct");
}),
TestCase ("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.ByContains("exam"), "exam WHERE data @> @criteria",
"By-Contains query not correct");
}),
TestCase("ByPathMach succeeds", () =>
{
Expect.equal(Postgres.Query.ByPathMatch("verify"), "verify WHERE data @? @path::jsonpath",
"By-JSON Path query not correct");
}) })
]) ])
]); ]);

View File

@ -67,6 +67,27 @@ public static class SqliteCSharpTests
{ {
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");
}), }),
TestCase("Patch succeeds", () =>
{
Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName),
$"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct");
}),
TestCase("RemoveFields succeeds", () =>
{
Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]),
$"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)",
"Field removal query not correct");
}),
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id",
"By-ID query not correct");
}),
TestCase("ByFields succeeds", () =>
{
Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]),
"unit WHERE data->>'That' > @field0", "By-Field query not correct");
}),
TestCase("Definition.EnsureTable succeeds", () => TestCase("Definition.EnsureTable succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"),

View File

@ -155,13 +155,21 @@ let all =
} }
] ]
] ]
testList "ParameterName" [ testList "FieldMatch.ToString" [
test "Derive succeeds with existing name" { test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName() let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco" 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" Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
} }
test "Derive succeeds with non-existent name" { test "succeeds with non-existent name" {
let name = ParameterName() let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned" 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) "@field1" "Counter should have advanced from previous call"

View File

@ -124,29 +124,6 @@ let unitTests =
Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" Expect.equal value (Sql.string "zed") "Maximum parameter value not correct"
} }
] ]
testList "addFieldParam" [
test "succeeds when a parameter is added" {
let paramList = addFieldParam "@field" (Field.EQ "it" "242") []
Expect.hasLength paramList 1 "There should have been a parameter added"
let name, value = Seq.head paramList
Expect.equal name "@field" "Field parameter name not correct"
Expect.equal value (Sql.string "242") "Parameter value not correct"
}
test "succeeds when a parameter is not added" {
let paramList = addFieldParam "@field" (Field.EX "tacos") []
Expect.isEmpty paramList "There should not have been any parameters added"
}
test "succeeds when two parameters are added" {
let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") []
Expect.hasLength paramList 2 "There should have been 2 parameters added"
let name, value = Seq.head paramList
Expect.equal name "@fieldmin" "Minimum field name not correct"
Expect.equal value (Sql.string "eh") "Minimum parameter value not correct"
let name, value = paramList |> Seq.skip 1 |> Seq.head
Expect.equal name "@fieldmax" "Maximum field name not correct"
Expect.equal value (Sql.string "zed") "Maximum parameter value not correct"
}
]
testList "fieldNameParams" [ testList "fieldNameParams" [
test "succeeds for one name" { test "succeeds for one name" {
let name, value = fieldNameParams [ "bob" ] let name, value = fieldNameParams [ "bob" ]
@ -261,6 +238,34 @@ let unitTests =
test "whereJsonPathMatches succeeds" { test "whereJsonPathMatches succeeds" {
Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
} }
test "patch succeeds" {
Expect.equal
(Query.patch PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data"
"Patch query not correct"
}
test "removeFields succeeds" {
Expect.equal
(Query.removeFields PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data - @name"
"Field removal query not correct"
}
test "byId succeeds" {
Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct"
}
test "byFields succeeds" {
Expect.equal
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE (data->>'That')::numeric > @field0"
"By-Field query not correct"
}
test "byContains succeeds" {
Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct"
}
test "byPathMach succeeds" {
Expect.equal
(Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query not correct"
}
] ]
] ]

View File

@ -55,6 +55,27 @@ let unitTests =
test "whereById succeeds" { test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct"
} }
test "patch succeeds" {
Expect.equal
(Query.patch SqliteDb.TableName)
$"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))"
"Patch query not correct"
}
test "removeFields succeeds" {
Expect.equal
(Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ])
$"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)"
"Field removal query not correct"
}
test "byId succeeds" {
Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct"
}
test "byFields succeeds" {
Expect.equal
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE data->>'That' > @field0"
"By-Field query not correct"
}
test "Definition.ensureTable succeeds" { test "Definition.ensureTable succeeds" {
Expect.equal Expect.equal
(Query.Definition.ensureTable "tbl") (Query.Definition.ensureTable "tbl")