From a4c17bdd7ef1c2f703261eb15dbcd81d7084c1d6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 26 Dec 2024 18:20:48 -0500 Subject: [PATCH] WIP on XML documentation --- src/Common/Library.fs | 323 +++++++++++++++++++++++++++++----------- src/Postgres/Library.fs | 69 ++++++--- 2 files changed, 283 insertions(+), 109 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index f51e81a..8bae732 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -2,43 +2,44 @@ open System.Security.Cryptography -/// The types of comparisons available for JSON fields +/// 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 (<=) + /// Less Than or Equal To (<=) | LessOrEqual of Value: obj - /// Not Equal to (<>) + /// 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 - /// Get the operator SQL for this comparison + /// The operator SQL for this comparison member this.OpSql = match this with | Equal _ -> "=" @@ -54,119 +55,190 @@ type Comparison = | NotExists -> "IS NULL" -/// The dialect in which a command should be rendered +/// The dialect in which a command should be rendered [] type Dialect = | PostgreSQL | 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 + /// The name of the field Name: string - /// The comparison for the field + /// The comparison for the field Comparison: Comparison - /// The name of the parameter for this field + /// The name of the parameter for this field ParameterName: string option - /// The table qualifier for this field + /// The table qualifier for this field Qualifier: string option } with - /// Create a comparison against a field + /// 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 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 + /// Create a Between field criterion + /// 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 Between<'T> name (min: 'T) (max: 'T) = Field.Where name (Between(min, max)) - /// Create a Between field criterion (alias) + /// 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 + /// 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) + /// 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 + /// 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 + /// The values for the comparison + /// 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 static member NameToPath (name: string) dialect format = let path = if name.Contains '.' then @@ -183,46 +255,59 @@ type Field = { match format with AsJson -> $"->'{name}'" | AsSql -> $"->>'{name}'" $"data{path}" - /// Create a field with a given name, but no other properties filled (op will be EQ, value will be "") + /// 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 static member Named name = Field.Where name (Equal "") - /// Specify the name of the parameter for this field + /// Specify the name of the parameter for this field + /// 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 + /// 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 member this.WithQualifier alias = { this with Qualifier = Some alias } - /// Get the qualified path to the 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 member this.Path dialect format = (this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect format -/// How fields should be matched +/// 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 + /// 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() = /// 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 + /// + /// The optional name of the parameter + /// The name of the parameter, derived if no name was provided member this.Derive paramName = match paramName with | Some it -> it @@ -231,31 +316,41 @@ type ParameterName() = $"@field{currentIdx}" -/// Automatically-generated document ID strategies +/// Automatically-generated document ID strategies [] type AutoId = - /// No automatic IDs will be generated + /// No automatic IDs will be generated | Disabled - /// Generate a MAX-plus-1 numeric value for documents + /// 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 + /// Generate a random string of hexadecimal characters for each document | RandomString with - /// Generate 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 + /// 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? + /// Does the given document need an automatic ID generated? + /// The auto-ID strategy currently in use + /// The document being inserted + /// The name of the ID property in the given document + /// True if an auto-ID strategy is implemented and the ID has no value, false otherwise + /// + /// If the ID field type and requested ID value are not compatible + /// static member NeedsAutoId<'T> strategy (document: 'T) idProp = match strategy with | Disabled -> false @@ -290,17 +385,17 @@ with | Disabled -> false -/// The required document serialization implementation +/// The required document serialization implementation type IDocumentSerializer = - /// Serialize an object to a JSON string + /// Serialize an object to a JSON string abstract Serialize<'T> : 'T -> string - /// Deserialize a JSON string into an object + /// Deserialize a JSON string into an object abstract Deserialize<'T> : string -> 'T -/// Document serializer defaults +/// Document serializer defaults module DocumentSerializer = open System.Text.Json @@ -312,7 +407,7 @@ module DocumentSerializer = o.Converters.Add(JsonFSharpConverter()) o - /// The default JSON serializer + /// The default JSON serializer [] let ``default`` = { new IDocumentSerializer with @@ -323,19 +418,21 @@ module DocumentSerializer = } -/// Configuration for document handling +/// Configuration for document handling [] 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 + /// Register a serializer to use for translating documents to domain types + /// The serializer to use when manipulating documents [] let useSerializer ser = serializerValue <- ser - /// Retrieve the currently configured serializer + /// Retrieve the currently configured serializer + /// The currently configured serializer [] let serializer () = serializerValue @@ -343,12 +440,14 @@ module Configuration = /// The serialized name of the ID field for documents let mutable private idFieldValue = "Id" - /// Specify the name of the ID field for documents + /// 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 + /// Retrieve the currently configured ID field for documents + /// The currently configured ID field [] let idField () = idFieldValue @@ -356,12 +455,14 @@ module Configuration = /// The automatic ID strategy used by the library let mutable private autoIdValue = Disabled - /// Specify the automatic ID generation strategy used by the library + /// 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 + /// Retrieve the currently configured automatic ID generation strategy + /// The current automatic ID generation strategy [] let autoIdStrategy () = autoIdValue @@ -369,30 +470,38 @@ module Configuration = /// The length of automatically generated random strings let mutable private idStringLengthValue = 16 - /// Specify the length of automatically generated random strings + /// 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 + /// Retrieve the currently configured length of automatically generated random strings + /// The current length of automatically generated random strings [] let idStringLength () = idStringLengthValue -/// Query construction functions +/// 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 [] let statementWhere statement where = $"%s{statement} WHERE %s{where}" - /// Queries to define tables and indexes + /// Queries to define tables and indexes module Definition = - /// SQL statement to create a document table + /// 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.) + /// A query to create a document table [] let ensureTableFor name dataType = $"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)" @@ -402,7 +511,12 @@ module Query = 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 + /// 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 + /// One or more fields to include in the index + /// The SQL dialect to use when creating this index + /// A query to create the field index [] let ensureIndexOn tableName indexName (fields: string seq) dialect = let _, tbl = splitSchemaAndTable tableName @@ -416,55 +530,84 @@ module Query = |> String.concat ", " $"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})" - /// SQL statement to create a key index for a document table + /// SQL statement to create a key index for a document table + /// The table on which a key index should be created (may include schema) + /// The SQL dialect to use when creating this index + /// A query to create the key index [] let ensureKey tableName dialect = (ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX") - /// Query to insert a document + /// Query to insert a document + /// The table into which to insert (may include schema) + /// A query to insert a document [] let insert tableName = $"INSERT INTO %s{tableName} VALUES (@data)" + /// /// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + /// + /// The table into which to save (may include schema) + /// A query to save a document [] let save tableName = sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - /// Query to count documents in a table (no WHERE clause) + /// 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 [] let count tableName = $"SELECT COUNT(*) AS it FROM %s{tableName}" - /// Query to check for document existence in a table + /// 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 + /// 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 (no WHERE clause) + /// 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 [] let find tableName = $"SELECT data FROM %s{tableName}" - /// Query to update a document (no WHERE clause) + /// 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 [] let update tableName = $"UPDATE %s{tableName} SET data = @data" - /// Query to delete documents from a table (no WHERE clause) + /// 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 [] let delete tableName = $"DELETE FROM %s{tableName}" - /// Create a SELECT clause to retrieve the document data from the given table + /// 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 [] [] 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 [] let orderBy fields dialect = if Seq.isEmpty fields then "" diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 39b3882..441117b 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -1,31 +1,36 @@ namespace BitBadger.Documents.Postgres -/// The type of index to generate for the document +/// The type of index to generate for the document [] type DocumentIndex = - /// A GIN index with standard operations (all operators supported) + /// A GIN index with standard operations (all operators supported) | Full - /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) + /// + /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) + /// | Optimized open Npgsql -/// Configuration for document handling +/// Configuration for document handling module Configuration = /// The data source to use for query execution let mutable private dataSourceValue : NpgsqlDataSource option = None - /// Register a data source to use for query execution (disposes the current one if it exists) + /// Register a data source to use for query execution (disposes the current one if it exists) + /// The data source to use [] let useDataSource source = if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose() dataSourceValue <- Some source - /// Retrieve the currently configured data source + /// Retrieve the currently configured data source + /// The current data source + /// If no data source has been configured [] let dataSource () = match dataSourceValue with @@ -69,21 +74,29 @@ module private Helpers = open BitBadger.Documents -/// Functions for creating parameters +/// Functions for creating parameters [] module Parameters = - /// Create an ID parameter (name "@id") + /// Create an ID parameter (name "@id") + /// The key value for the ID parameter + /// The name and parameter value for the ID [] let idParam (key: 'TKey) = "@id", parameterFor key (fun it -> Sql.string (string it)) - /// Create a parameter with a JSON value + /// Create a parameter with a JSON value + /// The name of the parameter to create + /// The criteria to provide as JSON + /// The name and parameter value for the JSON field [] let jsonParam (name: string) (it: 'TJson) = name, Sql.jsonb (Configuration.serializer().Serialize it) - /// Create JSON field parameters + /// Create JSON field parameters + /// The Fields to convert to parameters + /// The current parameters for the query + /// A unified sequence of parameter names and values [] let addFieldParams fields parameters = let name = ParameterName() @@ -114,23 +127,30 @@ module Parameters = |> Seq.toList |> Seq.ofList - /// Append JSON field name parameters for the given field names to the given parameters + /// Append JSON field name parameters for the given field names to the given parameters + /// The names of fields to be addressed + /// The name (@name) and parameter value for the field names [] let fieldNameParams (fieldNames: string seq) = if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames) else "@name", Sql.stringArray (Array.ofSeq fieldNames) - /// An empty parameter sequence + /// An empty parameter sequence [] let noParams = Seq.empty -/// Query construction functions +/// Query construction functions [] 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 + /// + /// How the fields should be matched + /// The fields for the comparisons + /// A WHERE clause implementing the comparisons for the given fields [] let whereByFields (howMatched: FieldMatch) fields = let name = ParameterName() @@ -159,27 +179,38 @@ module Query = else $"{it.Path PostgreSQL AsSql} {it.Comparison.OpSql} {param}") |> 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 + /// The ID of the document + /// A WHERE clause fragment identifying a document by its ID [] let whereById<'TKey> (docId: 'TKey) = whereByFields Any [ { Field.Equal (Configuration.idField ()) docId with ParameterName = Some "@id" } ] - /// Table and index definition queries + /// Table and index definition queries module Definition = - /// SQL statement to create a document table + /// SQL statement to create a document table + /// The name of the table (may include schema) + /// A query to create the table if it does not exist [] let ensureTable name = Query.Definition.ensureTableFor name "JSONB" - /// SQL statement to create an index on JSON documents in the specified table + /// SQL statement to create an index on JSON documents in the specified table + /// The name of the table to be indexed (may include schema) + /// The type of document index to create + /// A query to create the index if it does not exist [] let ensureDocumentIndex (name: string) idxType = let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" let tableName = name.Split '.' |> Array.last $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" - /// Create a WHERE clause fragment to implement a @> (JSON contains) condition + /// + /// Create a WHERE clause fragment to implement a @> (JSON contains) condition + /// + /// The parameter name for the query + /// A WHERE clause fragment for the contains condition [] let whereDataContains paramName = $"data @> %s{paramName}"