diff --git a/src/Common/BitBadger.Documents.Common.fsproj b/src/Common/BitBadger.Documents.Common.fsproj
index bded7fe..6e91b44 100644
--- a/src/Common/BitBadger.Documents.Common.fsproj
+++ b/src/Common/BitBadger.Documents.Common.fsproj
@@ -13,7 +13,8 @@
-
+
+
diff --git a/src/Common/Library.fs b/src/Common/Library.fs
index 56b6d1b..77dc4c1 100644
--- a/src/Common/Library.fs
+++ b/src/Common/Library.fs
@@ -15,6 +15,8 @@ type Op =
| LE
/// Not Equal to (<>)
| NE
+ /// Between (BETWEEN)
+ | BT
/// Exists (IS NOT NULL)
| EX
/// Does Not Exist (IS NULL)
@@ -28,6 +30,7 @@ type Op =
| LT -> "<"
| LE -> "<="
| NE -> "<>"
+ | BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
@@ -68,11 +71,15 @@ type Field = {
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value }
+ /// Create a BETWEEN field criterion
+ static member BT name (min: obj) (max: obj) =
+ { Name = name; Op = BT; Value = [ min; max ] }
+
/// Create an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj () }
- /// Create an not exists (IS NULL) field criterion
+ /// Create a not exists (IS NULL) field criterion
static member NEX name =
{ Name = name; Op = NEX; Value = obj () }
@@ -150,17 +157,6 @@ module Query =
let selectFromTable tableName =
$"SELECT data FROM %s{tableName}"
- /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
- []
- let whereByField field paramName =
- let theRest = match field.Op with EX | NEX -> string field.Op | _ -> $"{field.Op} %s{paramName}"
- $"data ->> '%s{field.Name}' {theRest}"
-
- /// Create a WHERE clause fragment to implement an ID-based query
- []
- let whereById paramName =
- whereByField (Field.EQ (Configuration.idField ()) 0) paramName
-
/// Queries to define tables and indexes
module Definition =
@@ -202,62 +198,6 @@ module Query =
[]
let save tableName =
sprintf
- "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data"
+ "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ())
-
- /// Query to update a document
- []
- let update tableName =
- $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
-
- /// Queries for counting documents
- module Count =
-
- /// Query to count all documents in a table
- []
- let all tableName =
- $"SELECT COUNT(*) AS it FROM %s{tableName}"
-
- /// Query to count matching documents using a text comparison on a JSON field
- []
- let byField tableName field =
- $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
-
- /// Queries for determining document existence
- module Exists =
-
- /// Query to determine if a document exists for the given ID
- []
- let byId tableName =
- $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
-
- /// Query to determine if documents exist using a comparison on a JSON field
- []
- let byField tableName field =
- $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
-
- /// Queries for retrieving documents
- module Find =
-
- /// Query to retrieve a document by its ID
- []
- let byId tableName =
- $"""{selectFromTable tableName} WHERE {whereById "@id"}"""
-
- /// Query to retrieve documents using a comparison on a JSON field
- []
- let byField tableName field =
- $"""{selectFromTable tableName} WHERE {whereByField field "@field"}"""
-
- /// Queries to delete documents
- module Delete =
-
- /// Query to delete a document by its ID
- []
- let byId tableName =
- $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
-
- /// Query to delete documents using a comparison on a JSON field
- []
- let byField tableName field =
- $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
+
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e8389bc..ea70aa3 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,11 +1,12 @@
- net6.0;net7.0;net8.0
+ net6.0;net8.0embeddedfalse
- 3.0.0.0
- 3.0.0.0
- 3.0.0
+ 3.1.0.0
+ 3.1.0.0
+ 3.1.0
+ Add BT (between) operator; drop .NET 7 supportdanieljsummersBit Badger SolutionsREADME.md
diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj
index a8eb158..316d2ae 100644
--- a/src/Postgres/BitBadger.Documents.Postgres.fsproj
+++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs
index 1351af8..cd551d4 100644
--- a/src/Postgres/Library.fs
+++ b/src/Postgres/Library.fs
@@ -63,17 +63,29 @@ module Parameters =
let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it)
- /// Create a JSON field parameter (name "@field")
+ /// Create a JSON field parameter
[]
let addFieldParam name field parameters =
match field.Op with
| EX | NEX -> parameters
+ | BT ->
+ let values = field.Value :?> obj list
+ List.concat
+ [ parameters
+ [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values)))
+ ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] ]
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) :: parameters
- /// Create a JSON field parameter (name "@field")
+ /// Create a JSON field parameter
let AddField name field parameters =
match field.Op with
| EX | NEX -> parameters
+ | BT ->
+ let values = field.Value :?> obj list
+ ResizeArray
+ [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values)))
+ ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ]
+ |> Seq.append parameters
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters
@@ -97,6 +109,25 @@ module Parameters =
[]
module Query =
+ /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
+ []
+ let whereByField field paramName =
+ match field.Op with
+ | EX | NEX -> $"data->>'{field.Name}' {field.Op}"
+ | BT ->
+ let names = $"{paramName}min AND {paramName}max"
+ let values = field.Value :?> obj list
+ match values[0] with
+ | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64
+ | :? decimal | :? single | :? double -> $"(data->>'{field.Name}')::numeric {field.Op} {names}"
+ | _ -> $"data->>'{field.Name}' {field.Op} {names}"
+ | _ -> $"data->>'{field.Name}' {field.Op} %s{paramName}"
+
+ /// Create a WHERE clause fragment to implement an ID-based query
+ []
+ let whereById paramName =
+ whereByField (Field.EQ (Configuration.idField ()) 0) paramName
+
/// Table and index definition queries
module Definition =
@@ -112,6 +143,11 @@ module Query =
let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
+ /// Query to update a document
+ []
+ let update tableName =
+ $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
+
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition
[]
let whereDataContains paramName =
@@ -125,6 +161,16 @@ module Query =
/// Queries for counting documents
module Count =
+ /// Query to count all documents in a table
+ []
+ let all tableName =
+ $"SELECT COUNT(*) AS it FROM %s{tableName}"
+
+ /// Query to count matching documents using a text comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
+
/// Query to count matching documents using a JSON containment query (@>)
[]
let byContains tableName =
@@ -138,6 +184,16 @@ module Query =
/// Queries for determining document existence
module Exists =
+ /// Query to determine if a document exists for the given ID
+ []
+ let byId tableName =
+ $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
+
+ /// Query to determine if documents exist using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
+
/// Query to determine if documents exist using a JSON containment query (@>)
[]
let byContains tableName =
@@ -151,6 +207,16 @@ module Query =
/// Queries for retrieving documents
module Find =
+ /// Query to retrieve a document by its ID
+ []
+ let byId tableName =
+ $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
+
+ /// Query to retrieve documents using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
+
/// Query to retrieve documents using a JSON containment query (@>)
[]
let byContains tableName =
@@ -171,12 +237,12 @@ module Query =
/// Query to patch a document by its ID
[]
let byId tableName =
- Query.whereById "@id" |> update tableName
+ whereById "@id" |> update tableName
/// Query to patch documents match a JSON field comparison (->> =)
[]
let byField tableName field =
- Query.whereByField field "@field" |> update tableName
+ whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[]
@@ -198,12 +264,12 @@ module Query =
/// Query to remove fields from a document by the document's ID
[]
let byId tableName =
- Query.whereById "@id" |> update tableName
+ whereById "@id" |> update tableName
/// Query to remove fields from documents via a comparison on a JSON field within the document
[]
let byField tableName field =
- Query.whereByField field "@field" |> update tableName
+ whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[]
@@ -218,6 +284,16 @@ module Query =
/// Queries to delete documents
module Delete =
+ /// Query to delete a document by its ID
+ []
+ let byId tableName =
+ $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
+
+ /// Query to delete documents using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
+
/// Query to delete documents using a JSON containment query (@>)
[]
let byContains tableName =
diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj
index b6009bb..14a006d 100644
--- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj
+++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj
@@ -14,7 +14,8 @@
-
+
+
diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs
index 8e7cdb7..27815f8 100644
--- a/src/Sqlite/Library.fs
+++ b/src/Sqlite/Library.fs
@@ -31,6 +31,21 @@ module Configuration =
[]
module Query =
+ /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
+ []
+ let whereByField field paramName =
+ let theRest =
+ match field.Op with
+ | EX | NEX -> ""
+ | BT -> $" {paramName}min AND {paramName}max"
+ | _ -> $" %s{paramName}"
+ $"data->>'{field.Name}' {field.Op}{theRest}"
+
+ /// Create a WHERE clause fragment to implement an ID-based query
+ []
+ let whereById paramName =
+ whereByField (Field.EQ (Configuration.idField ()) 0) paramName
+
/// Data definition
module Definition =
@@ -39,6 +54,50 @@ module Query =
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
+ /// Query to update a document
+ []
+ let update tableName =
+ $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
+
+ /// Queries for counting documents
+ module Count =
+
+ /// Query to count all documents in a table
+ []
+ let all tableName =
+ $"SELECT COUNT(*) AS it FROM %s{tableName}"
+
+ /// Query to count matching documents using a text comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
+
+ /// Queries for determining document existence
+ module Exists =
+
+ /// Query to determine if a document exists for the given ID
+ []
+ let byId tableName =
+ $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
+
+ /// Query to determine if documents exist using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
+
+ /// Queries for retrieving documents
+ module Find =
+
+ /// Query to retrieve a document by its ID
+ []
+ let byId tableName =
+ $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
+
+ /// Query to retrieve documents using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
+
/// Document patching (partial update) queries
module Patch =
@@ -49,12 +108,12 @@ module Query =
/// Query to patch (partially update) a document by its ID
[]
let byId tableName =
- Query.whereById "@id" |> update tableName
+ whereById "@id" |> update tableName
/// Query to patch (partially update) a document via a comparison on a JSON field
[]
let byField tableName field =
- Query.whereByField field "@field" |> update tableName
+ whereByField field "@field" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
@@ -67,7 +126,7 @@ module Query =
/// Query to remove fields from a document by the document's ID
[]
let byId tableName parameters =
- Query.whereById "@id" |> update tableName parameters
+ whereById "@id" |> update tableName parameters
/// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) =
@@ -76,12 +135,25 @@ module Query =
/// Query to remove fields from documents via a comparison on a JSON field within the document
[]
let byField tableName field parameters =
- Query.whereByField field "@field" |> update tableName parameters
+ whereByField field "@field" |> update tableName parameters
/// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters)
+ /// Queries to delete documents
+ module Delete =
+
+ /// Query to delete a document by its ID
+ []
+ let byId tableName =
+ $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
+
+ /// Query to delete documents using a comparison on a JSON field
+ []
+ let byField tableName field =
+ $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
+
/// Parameter handling helpers
[]
@@ -100,12 +172,26 @@ module Parameters =
/// Create a JSON field parameter (name "@field")
[]
let addFieldParam name field parameters =
- match field.Op with EX | NEX -> parameters | _ -> SqliteParameter(name, field.Value) :: parameters
+ match field.Op with
+ | EX | NEX -> parameters
+ | BT ->
+ let values = field.Value :?> obj list
+ SqliteParameter($"{name}min", values[0]) :: SqliteParameter($"{name}max", values[1]) :: parameters
+ | _ -> SqliteParameter(name, field.Value) :: parameters
/// Create a JSON field parameter (name "@field")
let AddField(name, field, parameters) =
match field.Op with
| EX | NEX -> parameters
+ | BT ->
+ let values = field.Value :?> obj list
+ // let min = SqliteParameter($"{name}min", SqliteType.Integer)
+ // min.Value <- values[0]
+ // let max = SqliteParameter($"{name}max", SqliteType.Integer)
+ // max.Value <- values[1]
+ [ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ]
+ // [min; max]
+ |> Seq.append parameters
| _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters
diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj
index 489d298..951b6d7 100644
--- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj
+++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs
index 1426c3c..fd8162a 100644
--- a/src/Tests.CSharp/CommonCSharpTests.cs
+++ b/src/Tests.CSharp/CommonCSharpTests.cs
@@ -1,5 +1,6 @@
using Expecto.CSharp;
using Expecto;
+using Microsoft.FSharp.Collections;
namespace BitBadger.Documents.Tests.CSharp;
@@ -96,6 +97,10 @@ public static class CommonCSharpTests
{
Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct");
}),
+ TestCase("BT succeeds", () =>
+ {
+ Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct");
+ }),
TestCase("EX succeeds", () =>
{
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct");
@@ -149,6 +154,13 @@ public static class CommonCSharpTests
Expect.equal(field.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value incorrect");
}),
+ TestCase("BT succeeds", () =>
+ {
+ var field = Field.BT("Age", 18, 49);
+ Expect.equal(field.Name, "Age", "Field name incorrect");
+ Expect.equal(field.Op, Op.BT, "Operator incorrect");
+ Expect.equal(((FSharpList