diff --git a/src/Common/Library.fs b/src/Common/Library.fs
index 8bae732..c5668b7 100644
--- a/src/Common/Library.fs
+++ b/src/Common/Library.fs
@@ -5,40 +5,40 @@ open System.Security.Cryptography
/// The types of comparisons available for JSON fields
///
type Comparison =
-
- /// Equals (=)
+
+ /// Equals (=)
| Equal of Value: obj
-
- /// Greater Than (>)
+
+ /// Greater Than (>)
| Greater of Value: obj
-
- /// Greater Than or Equal To (>=)
+
+ /// Greater Than or Equal To (>=)
| GreaterOrEqual of Value: obj
-
- /// Less Than (<)
+
+ /// Less Than (<)
| Less of Value: obj
-
- /// Less Than or Equal To (<=)
- | LessOrEqual of Value: obj
-
- /// Not Equal to (<>)
+
+ /// Less Than or Equal To (<=)
+ | LessOrEqual of Value: obj
+
+ /// Not Equal to (<>)
| NotEqual of Value: obj
-
- /// Between (BETWEEN)
+
+ /// Between (BETWEEN)
| Between of Min: obj * Max: obj
-
- /// In (IN)
+
+ /// In (IN)
| In of Values: obj seq
-
- /// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
+
+ /// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
| InArray of Table: string * Values: obj seq
-
- /// Exists (IS NOT NULL)
+
+ /// Exists (IS NOT NULL)
| Exists
-
- /// Does Not Exist (IS NULL)
+
+ /// Does Not Exist (IS NULL)
| NotExists
-
+
/// The operator SQL for this comparison
member this.OpSql =
match this with
@@ -50,7 +50,7 @@ type Comparison =
| NotEqual _ -> "<>"
| Between _ -> "BETWEEN"
| In _ -> "IN"
- | InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
+ | InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
| Exists -> "IS NOT NULL"
| NotExists -> "IS NULL"
@@ -62,120 +62,120 @@ type Dialect =
| SQLite
-/// The format in which an element of a JSON field should be extracted
+/// The format in which an element of a JSON field should be extracted
[]
type FieldFormat =
-
+
///
- /// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
+ /// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
///
| AsSql
-
- /// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
+
+ /// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
| AsJson
-/// Criteria for a field WHERE clause
+/// Criteria for a field WHERE clause
type Field = {
-
+
/// The name of the field
Name: string
-
+
/// The comparison for the field
Comparison: Comparison
-
+
/// The name of the parameter for this field
ParameterName: string option
-
+
/// The table qualifier for this field
Qualifier: string option
} with
-
+
/// Create a comparison against a field
/// The name of the field against which the comparison should be applied
/// The comparison for the given field
- /// A new Field instance implementing the given comparison
+ /// A new Field instance implementing the given comparison
static member Where name (comparison: Comparison) =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
-
- /// Create an equals (=) field criterion
+
+ /// Create an equals (=) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member Equal<'T> name (value: 'T) =
Field.Where name (Equal value)
-
- /// Create an equals (=) field criterion (alias)
+
+ /// Create an equals (=) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member EQ<'T> name (value: 'T) = Field.Equal name value
-
- /// Create a greater than (>) field criterion
+
+ /// Create a greater than (>) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member Greater<'T> name (value: 'T) =
Field.Where name (Greater value)
-
- /// Create a greater than (>) field criterion (alias)
+
+ /// Create a greater than (>) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member GT<'T> name (value: 'T) = Field.Greater name value
-
- /// Create a greater than or equal to (>=) field criterion
+
+ /// Create a greater than or equal to (>=) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member GreaterOrEqual<'T> name (value: 'T) =
Field.Where name (GreaterOrEqual value)
-
- /// Create a greater than or equal to (>=) field criterion (alias)
+
+ /// Create a greater than or equal to (>=) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member GE<'T> name (value: 'T) = Field.GreaterOrEqual name value
-
- /// Create a less than (<) field criterion
+
+ /// Create a less than (<) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member Less<'T> name (value: 'T) =
Field.Where name (Less value)
-
- /// Create a less than (<) field criterion (alias)
+
+ /// Create a less than (<) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member LT<'T> name (value: 'T) = Field.Less name value
-
- /// Create a less than or equal to (<=) field criterion
+
+ /// Create a less than or equal to (<=) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member LessOrEqual<'T> name (value: 'T) =
Field.Where name (LessOrEqual value)
-
- /// Create a less than or equal to (<=) field criterion (alias)
+
+ /// Create a less than or equal to (<=) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member LE<'T> name (value: 'T) = Field.LessOrEqual name value
-
- /// Create a not equals (<>) field criterion
+
+ /// Create a not equals (<>) field criterion
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member NotEqual<'T> name (value: 'T) =
Field.Where name (NotEqual value)
-
- /// Create a not equals (<>) field criterion (alias)
+
+ /// Create a not equals (<>) field criterion (alias)
/// The name of the field to be compared
/// The value for the comparison
/// A field with the given comparison
static member NE<'T> name (value: 'T) = Field.NotEqual name value
-
+
/// Create a Between field criterion
/// The name of the field to be compared
/// The minimum value for the comparison range
@@ -183,27 +183,27 @@ type Field = {
/// A field with the given comparison
static member Between<'T> name (min: 'T) (max: 'T) =
Field.Where name (Between(min, max))
-
+
/// Create a Between field criterion (alias)
/// The name of the field to be compared
/// The minimum value for the comparison range
/// The maximum value for the comparison range
/// A field with the given comparison
static member BT<'T> name (min: 'T) (max: 'T) = Field.Between name min max
-
+
/// Create an In field criterion
/// The name of the field to be compared
/// The values for the comparison
/// A field with the given comparison
static member In<'T> name (values: 'T seq) =
Field.Where name (In (Seq.map box values))
-
+
/// Create an In field criterion (alias)
/// The name of the field to be compared
/// The values for the comparison
/// A field with the given comparison
static member IN<'T> name (values: 'T seq) = Field.In name values
-
+
/// Create an InArray field criterion
/// The name of the field to be compared
/// The name of the table in which the field's documents are stored
@@ -211,34 +211,34 @@ type Field = {
/// A field with the given comparison
static member InArray<'T> name tableName (values: 'T seq) =
Field.Where name (InArray(tableName, Seq.map box values))
-
- /// Create an exists (IS NOT NULL) field criterion
+
+ /// Create an exists (IS NOT NULL) field criterion
/// The name of the field to be compared
/// A field with the given comparison
static member Exists name =
Field.Where name Exists
-
- /// Create an exists (IS NOT NULL) field criterion (alias)
+
+ /// Create an exists (IS NOT NULL) field criterion (alias)
/// The name of the field to be compared
/// A field with the given comparison
static member EX name = Field.Exists name
-
- /// Create a not exists (IS NULL) field criterion
+
+ /// Create a not exists (IS NULL) field criterion
/// The name of the field to be compared
/// A field with the given comparison
static member NotExists name =
Field.Where name NotExists
-
- /// Create a not exists (IS NULL) field criterion (alias)
+
+ /// Create a not exists (IS NULL) field criterion (alias)
/// The name of the field to be compared
/// A field with the given comparison
static member NEX name = Field.NotExists name
-
- /// Transform a field name (a.b.c) to a path for the given SQL dialect
+
+ /// Transform a field name (a.b.c) to a path for the given SQL dialect
/// The name of the field in dotted format
/// The SQL dialect to use when converting the name to nested path format
/// Whether to reference this path as a JSON value or a SQL value
- /// A string with the path required to address the nested document value
+ /// A string with the path required to address the nested document value
static member NameToPath (name: string) dialect format =
let path =
if name.Contains '.' then
@@ -254,19 +254,19 @@ type Field = {
else
match format with AsJson -> $"->'{name}'" | AsSql -> $"->>'{name}'"
$"data{path}"
-
+
/// Create a field with a given name, but no other properties filled
/// The field name, along with any other qualifications if used in a sorting context
- /// Comparison will be Equal, value will be an empty string
+ /// Comparison will be Equal, value will be an empty string
static member Named name =
Field.Where name (Equal "")
-
+
/// Specify the name of the parameter for this field
- /// The parameter name (including : or @)
+ /// The parameter name (including : or @)
/// A field with the given parameter name specified
member this.WithParameterName name =
{ this with ParameterName = Some name }
-
+
/// Specify a qualifier (alias) for the table from which this field will be referenced
/// The table alias for this field comparison
/// A field with the given qualifier specified
@@ -276,7 +276,7 @@ type Field = {
/// Get the qualified path to the field
/// The SQL dialect to use when converting the name to nested path format
/// Whether to reference this path as a JSON value or a SQL value
- /// A string with the qualified path required to address the nested document value
+ /// A string with the qualified path required to address the nested document value
member this.Path dialect format =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "")
+ Field.NameToPath this.Name dialect format
@@ -285,13 +285,13 @@ type Field = {
/// How fields should be matched
[]
type FieldMatch =
-
- /// Any field matches (OR)
+
+ /// Any field matches (OR)
| Any
-
- /// All fields match (AND)
+
+ /// All fields match (AND)
| All
-
+
/// The SQL value implementing each matching strategy
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
@@ -299,10 +299,10 @@ type FieldMatch =
/// 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
///
@@ -319,30 +319,30 @@ type ParameterName() =
/// Automatically-generated document ID strategies
[]
type AutoId =
-
+
/// No automatic IDs will be generated
| Disabled
-
+
/// Generate a MAX-plus-1 numeric value for documents
| Number
-
- /// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
+
+ /// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
| Guid
-
+
/// Generate a random string of hexadecimal characters for each document
| RandomString
with
- /// Generate a GUID string
- /// A GUID string
+ /// Generate a GUID string
+ /// A GUID string
static member GenerateGuid() =
System.Guid.NewGuid().ToString "N"
-
+
/// Generate a string of random hexadecimal characters
/// The number of characters to generate
/// A string of the given length with random hexadecimal characters
static member GenerateRandomString(length: int) =
RandomNumberGenerator.GetHexString(length, lowercase = true)
-
+
/// Does the given document need an automatic ID generated?
/// The auto-ID strategy currently in use
/// The document being inserted
@@ -387,26 +387,26 @@ with
/// The required document serialization implementation
type IDocumentSerializer =
-
+
/// Serialize an object to a JSON string
abstract Serialize<'T> : 'T -> string
-
+
/// Deserialize a JSON string into an object
abstract Deserialize<'T> : string -> 'T
/// Document serializer defaults
module DocumentSerializer =
-
+
open System.Text.Json
open System.Text.Json.Serialization
-
+
/// The default JSON serializer options to use with the stock serializer
let private jsonDefaultOpts =
let o = JsonSerializerOptions()
o.Converters.Add(JsonFSharpConverter())
o
-
+
/// The default JSON serializer
[]
let ``default`` =
@@ -424,7 +424,7 @@ module Configuration =
/// The serializer to use for document manipulation
let mutable private serializerValue = DocumentSerializer.``default``
-
+
/// Register a serializer to use for translating documents to domain types
/// The serializer to use when manipulating documents
[]
@@ -436,46 +436,46 @@ module Configuration =
[]
let serializer () =
serializerValue
-
+
/// The serialized name of the ID field for documents
let mutable private idFieldValue = "Id"
-
+
/// Specify the name of the ID field for documents
/// The name of the ID field for documents
[]
let useIdField it =
idFieldValue <- it
-
+
/// Retrieve the currently configured ID field for documents
/// The currently configured ID field
[]
let idField () =
idFieldValue
-
+
/// The automatic ID strategy used by the library
let mutable private autoIdValue = Disabled
-
+
/// Specify the automatic ID generation strategy used by the library
/// The automatic ID generation strategy to use
[]
let useAutoIdStrategy it =
autoIdValue <- it
-
+
/// Retrieve the currently configured automatic ID generation strategy
/// The current automatic ID generation strategy
[]
let autoIdStrategy () =
autoIdValue
-
+
/// The length of automatically generated random strings
let mutable private idStringLengthValue = 16
-
+
/// Specify the length of automatically generated random strings
/// The length of automatically generated random strings
[]
let useIdStringLength length =
idStringLengthValue <- length
-
+
/// Retrieve the currently configured length of automatically generated random strings
/// The current length of automatically generated random strings
[]
@@ -486,31 +486,31 @@ module Configuration =
/// Query construction functions
[]
module Query =
-
- /// Combine a query (SELECT, UPDATE, etc.) and a WHERE clause
+
+ /// Combine a query (SELECT, UPDATE, etc.) and a WHERE clause
/// The first part of the statement
- /// The WHERE clause for the statement
- /// The two parts of the query combined with WHERE
+ /// The WHERE clause for the statement
+ /// The two parts of the query combined with WHERE
[]
let statementWhere statement where =
$"%s{statement} WHERE %s{where}"
-
+
/// Queries to define tables and indexes
module Definition =
-
+
/// SQL statement to create a document table
/// The name of the table to create (may include schema)
- /// The type of data for the column (JSON, JSONB, etc.)
+ /// The type of data for the column (JSON, JSONB, etc.)
/// A query to create a document table
[]
let ensureTableFor name dataType =
$"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)"
-
+
/// Split a schema and table name
let private splitSchemaAndTable (tableName: string) =
let parts = tableName.Split '.'
if Array.length parts = 1 then "", tableName else parts[0], parts[1]
-
+
/// SQL statement to create an index on one or more fields in a JSON document
/// The table on which an index should be created (may include schema)
/// The name of the index to be created
@@ -537,7 +537,7 @@ module Query =
[]
let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
-
+
/// Query to insert a document
/// The table into which to insert (may include schema)
/// A query to insert a document
@@ -554,48 +554,48 @@ module Query =
let save tableName =
sprintf
"INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data"
- tableName (Configuration.idField ())
-
+ tableName (Configuration.idField ())
+
/// Query to count documents in a table
/// The table in which to count documents (may include schema)
/// A query to count documents
- /// This query has no WHERE clause
+ /// This query has no WHERE clause
[]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
-
+
/// Query to check for document existence in a table
/// The table in which existence should be checked (may include schema)
- /// The WHERE clause with the existence criteria
+ /// The WHERE clause with the existence criteria
/// A query to check document existence
[]
let exists tableName where =
$"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it"
-
+
/// Query to select documents from a table
/// The table from which documents should be found (may include schema)
/// A query to retrieve documents
- /// This query has no WHERE clause
+ /// This query has no WHERE clause
[]
let find tableName =
$"SELECT data FROM %s{tableName}"
-
+
/// Query to update (replace) a document
/// The table in which documents should be replaced (may include schema)
/// A query to update documents
- /// This query has no WHERE clause
+ /// This query has no WHERE clause
[]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
-
+
/// Query to delete documents from a table
/// The table in which documents should be deleted (may include schema)
/// A query to delete documents
- /// This query has no WHERE clause
+ /// This query has no WHERE clause
[]
let delete tableName =
$"DELETE FROM %s{tableName}"
-
+
/// Create a SELECT clause to retrieve the document data from the given table
/// The table from which documents should be found (may include schema)
/// A query to retrieve documents
@@ -603,11 +603,11 @@ module Query =
[]
let selectFromTable tableName =
find tableName
-
- /// Create an ORDER BY clause for the given fields
+
+ /// Create an ORDER BY clause for the given fields
/// One or more fields by which to order
/// The SQL dialect for the generated clause
- /// An ORDER BY clause for the given fields
+ /// An ORDER BY clause for the given fields
[]
let orderBy fields dialect =
if Seq.isEmpty fields then ""
diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs
index 911b732..90cbcba 100644
--- a/src/Tests.CSharp/CommonCSharpTests.cs
+++ b/src/Tests.CSharp/CommonCSharpTests.cs
@@ -16,7 +16,7 @@ internal class TestSerializer : IDocumentSerializer
}
///
-/// C# Tests for common functionality in BitBadger.Documents
+/// C# Tests for common functionality in BitBadger.Documents
///
public static class CommonCSharpTests
{
@@ -417,7 +417,7 @@ public static class CommonCSharpTests
})
])
]);
-
+
///
/// Unit tests for the Configuration static class
///
@@ -647,7 +647,7 @@ public static class CommonCSharpTests
})
])
]);
-
+
///
/// Unit tests
///
diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs
index df8ab6d..b0b3393 100644
--- a/src/Tests.CSharp/PostgresCSharpTests.cs
+++ b/src/Tests.CSharp/PostgresCSharpTests.cs
@@ -344,6 +344,55 @@ public static class PostgresCSharpTests
return reader.ReadToEnd();
}
+ /// Verify a JSON array begins with "[" and ends with "]"
+ private static void VerifyBeginEnd(string json)
+ {
+ Expect.stringStarts(json, "[", "The array should have started with `[`");
+ Expect.stringEnds(json, "]", "The array should have ended with `]`");
+ }
+
+ /// Verify the presence of a document by its ID
+ private static void VerifyDocById(string json, string docId)
+ {
+ Expect.stringContains(json, $"{{\"Id\": \"{docId}\",", $"Document `{docId}` not present");
+ }
+
+ /// Verify the presence of a document by its ID
+ private static void VerifySingleById(string json, string docId)
+ {
+ VerifyBeginEnd(json);
+ Expect.stringContains(json, $"{{\"Id\": \"{docId}\",", $"Document `{docId}` not present");
+ }
+
+ /// Verify the presence of any of the given document IDs in the given JSON
+ private static void VerifyAnyById(string json, IEnumerable docIds)
+ {
+ var theIds = docIds.ToList();
+ if (theIds.Any(it => json.Contains($"{{\"Id\": \"{it}\""))) return;
+ var ids = string.Join(", ", theIds);
+ Expect.isTrue(false, $"Could not find any of IDs {ids} in {json}");
+ }
+
+ /// Verify the JSON for `all` returning data
+ private static void VerifyAllData(string json)
+ {
+ VerifyBeginEnd(json);
+ IEnumerable ids = ["one", "two", "three", "four", "five"];
+ foreach (var docId in ids) VerifyDocById(json, docId);
+ }
+
+ /// Verify an empty JSON array
+ private static void VerifyEmpty(string json)
+ {
+ Expect.equal(json, "[]", "There should be no documents returned");
+ }
+
+ /// Verify an empty JSON document
+ private static void VerifyNoDoc(string json)
+ {
+ Expect.equal(json, "{}", "There should be no document returned");
+ }
+
///
/// Integration tests for the Configuration module of the PostgreSQL library
///
@@ -1291,55 +1340,6 @@ public static class PostgresCSharpTests
])
]);
- /// Verify a JSON array begins with "[" and ends with "]"
- private static void VerifyBeginEnd(string json)
- {
- Expect.stringStarts(json, "[", "The array should have started with `[`");
- Expect.stringEnds(json, "]", "The array should have ended with `]`");
- }
-
- /// Verify the presence of a document by its ID
- private static void VerifyDocById(string json, string docId)
- {
- Expect.stringContains(json, $"{{\"Id\": \"{docId}\",", $"Document `{docId}` not present");
- }
-
- /// Verify the presence of a document by its ID
- private static void VerifySingleById(string json, string docId)
- {
- VerifyBeginEnd(json);
- Expect.stringContains(json, $"{{\"Id\": \"{docId}\",", $"Document `{docId}` not present");
- }
-
- /// Verify the presence of any of the given document IDs in the given JSON
- private static void VerifyAnyById(string json, IEnumerable docIds)
- {
- var theIds = docIds.ToList();
- if (theIds.Any(it => json.Contains($"{{\"Id\": \"{it}\""))) return;
- var ids = string.Join(", ", theIds);
- Expect.isTrue(false, $"Could not find any of IDs {ids} in {json}");
- }
-
- /// Verify the JSON for `all` returning data
- private static void VerifyAllData(string json)
- {
- VerifyBeginEnd(json);
- IEnumerable ids = ["one", "two", "three", "four", "five"];
- foreach (var docId in ids) VerifyDocById(json, docId);
- }
-
- /// Verify an empty JSON array
- private static void VerifyEmpty(string json)
- {
- Expect.equal(json, "[]", "There should be no documents returned");
- }
-
- /// Verify an empty JSON document
- private static void VerifyNoDoc(string json)
- {
- Expect.equal(json, "{}", "There should be no document returned");
- }
-
/// Verify the JSON for an ordered query
private static void VerifyExpectedOrder(string json, string idFirst, string idSecond, string? idThird = null,
string? idFourth = null, string? idFifth = null)