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)