WIP on XML documentation

This commit is contained in:
Daniel J. Summers 2024-12-26 18:20:48 -05:00
parent 147a72b476
commit a4c17bdd7e
2 changed files with 283 additions and 109 deletions

View File

@ -2,43 +2,44 @@
open System.Security.Cryptography
/// The types of comparisons available for JSON fields
/// <summary>The types of comparisons available for JSON fields</summary>
/// <exclude />
type Comparison =
/// Equals (=)
/// <summary>Equals (<tt>=</tt>)</summary>
| Equal of Value: obj
/// Greater Than (>)
/// <summary>Greater Than (<tt>&gt;</tt>)</summary>
| Greater of Value: obj
/// Greater Than or Equal To (>=)
/// <summary>Greater Than or Equal To (<tt>&gt;=</tt>)</summary>
| GreaterOrEqual of Value: obj
/// Less Than (<)
/// <summary>Less Than (<tt>&lt;</tt>)</summary>
| Less of Value: obj
/// Less Than or Equal To (<=)
/// <summary>Less Than or Equal To (<tt>&lt;=</tt>)</summary>
| LessOrEqual of Value: obj
/// Not Equal to (<>)
/// <summary>Not Equal to (<tt>&lt;&gt;</tt>)</summary>
| NotEqual of Value: obj
/// Between (BETWEEN)
/// <summary>Between (<tt>BETWEEN</tt>)</summary>
| Between of Min: obj * Max: obj
/// In (IN)
/// <summary>In (<tt>IN</tt>)</summary>
| In of Values: obj seq
/// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
/// <summary>In Array (PostgreSQL: <tt>|?</tt>, SQLite: <tt>EXISTS / json_each / IN</tt>)</summary>
| InArray of Table: string * Values: obj seq
/// Exists (IS NOT NULL)
/// <summary>Exists (<tt>IS NOT NULL</tt>)</summary>
| Exists
/// Does Not Exist (IS NULL)
/// <summary>Does Not Exist (<tt>IS NULL</tt>)</summary>
| NotExists
/// Get the operator SQL for this comparison
/// <summary>The operator SQL for this comparison</summary>
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
/// <summary>The dialect in which a command should be rendered</summary>
[<Struct>]
type Dialect =
| PostgreSQL
| SQLite
/// The format in which an element of a JSON field should be extracted
/// <summary>The format in which an element of a JSON field should be extracted</summary>
[<Struct>]
type FieldFormat =
/// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
/// <summary>
/// Use <tt>-&gt;&gt;</tt> or <tt>#&gt;&gt;</tt>; extracts a text (PostgreSQL) or SQL (SQLite) value
/// </summary>
| AsSql
/// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
/// <summary>Use <tt>-&gt;</tt> or <tt>#&gt;</tt>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value</summary>
| AsJson
/// Criteria for a field WHERE clause
/// <summary>Criteria for a field <tt>WHERE</tt> clause</summary>
type Field = {
/// The name of the field
/// <summary>The name of the field</summary>
Name: string
/// The comparison for the field
/// <summary>The comparison for the field</summary>
Comparison: Comparison
/// The name of the parameter for this field
/// <summary>The name of the parameter for this field</summary>
ParameterName: string option
/// The table qualifier for this field
/// <summary>The table qualifier for this field</summary>
Qualifier: string option
} with
/// Create a comparison against a field
/// <summary>Create a comparison against a field</summary>
/// <param name="name">The name of the field against which the comparison should be applied</param>
/// <param name="comparison">The comparison for the given field</param>
/// <returns>A new <tt>Field</tt> instance implementing the given comparison</returns>
static member Where name (comparison: Comparison) =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
/// Create an equals (=) field criterion
/// <summary>Create an equals (<tt>=</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Equal<'T> name (value: 'T) =
Field.Where name (Equal value)
/// Create an equals (=) field criterion (alias)
/// <summary>Create an equals (<tt>=</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member EQ<'T> name (value: 'T) = Field.Equal name value
/// Create a greater than (>) field criterion
/// <summary>Create a greater than (<tt>&gt;</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Greater<'T> name (value: 'T) =
Field.Where name (Greater value)
/// Create a greater than (>) field criterion (alias)
/// <summary>Create a greater than (<tt>&gt;</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GT<'T> name (value: 'T) = Field.Greater name value
/// Create a greater than or equal to (>=) field criterion
/// <summary>Create a greater than or equal to (<tt>&gt;=</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GreaterOrEqual<'T> name (value: 'T) =
Field.Where name (GreaterOrEqual value)
/// Create a greater than or equal to (>=) field criterion (alias)
/// <summary>Create a greater than or equal to (<tt>&gt;=</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GE<'T> name (value: 'T) = Field.GreaterOrEqual name value
/// Create a less than (<) field criterion
/// <summary>Create a less than (<tt>&lt;</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Less<'T> name (value: 'T) =
Field.Where name (Less value)
/// Create a less than (<) field criterion (alias)
/// <summary>Create a less than (<tt>&lt;</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LT<'T> name (value: 'T) = Field.Less name value
/// Create a less than or equal to (<=) field criterion
/// <summary>Create a less than or equal to (<tt>&lt;=</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LessOrEqual<'T> name (value: 'T) =
Field.Where name (LessOrEqual value)
/// Create a less than or equal to (<=) field criterion (alias)
/// <summary>Create a less than or equal to (<tt>&lt;=</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LE<'T> name (value: 'T) = Field.LessOrEqual name value
/// Create a not equals (<>) field criterion
/// <summary>Create a not equals (<tt>&lt;&gt;</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member NotEqual<'T> name (value: 'T) =
Field.Where name (NotEqual value)
/// Create a not equals (<>) field criterion (alias)
/// <summary>Create a not equals (<tt>&lt;&gt;</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member NE<'T> name (value: 'T) = Field.NotEqual name value
/// Create a Between field criterion
/// <summary>Create a Between field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="min">The minimum value for the comparison range</param>
/// <param name="max">The maximum value for the comparison range</param>
/// <returns>A field with the given comparison</returns>
static member Between<'T> name (min: 'T) (max: 'T) =
Field.Where name (Between(min, max))
/// Create a Between field criterion (alias)
/// <summary>Create a Between field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="min">The minimum value for the comparison range</param>
/// <param name="max">The maximum value for the comparison range</param>
/// <returns>A field with the given comparison</returns>
static member BT<'T> name (min: 'T) (max: 'T) = Field.Between name min max
/// Create an In field criterion
/// <summary>Create an In field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member In<'T> name (values: 'T seq) =
Field.Where name (In (Seq.map box values))
/// Create an In field criterion (alias)
/// <summary>Create an In field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member IN<'T> name (values: 'T seq) = Field.In name values
/// Create an InArray field criterion
/// <summary>Create an InArray field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="tableName">The name of the table in which the field's documents are stored</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
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
/// <summary>Create an exists (<tt>IS NOT NULL</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member Exists name =
Field.Where name Exists
/// Create an exists (IS NOT NULL) field criterion (alias)
/// <summary>Create an exists (<tt>IS NOT NULL</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member EX name = Field.Exists name
/// Create a not exists (IS NULL) field criterion
/// <summary>Create a not exists (<tt>IS NULL</tt>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member NotExists name =
Field.Where name NotExists
/// Create a not exists (IS NULL) field criterion (alias)
/// <summary>Create a not exists (<tt>IS NULL</tt>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member NEX name = Field.NotExists name
/// Transform a field name (a.b.c) to a path for the given SQL dialect
/// <summary>Transform a field name (<tt>a.b.c</tt>) to a path for the given SQL dialect</summary>
/// <param name="name">The name of the field in dotted format</param>
/// <param name="dialect">The SQL dialect to use when converting the name to nested path format</param>
/// <param name="format">Whether to reference this path as a JSON value or a SQL value</param>
/// <returns>A <tt>string</tt> with the path required to address the nested document value</returns>
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 "")
/// <summary>Create a field with a given name, but no other properties filled</summary>
/// <param name="name">The field name, along with any other qualifications if used in a sorting context</param>
/// <remarks><tt>Comparison</tt> will be <tt>Equal</tt>, value will be an empty string</remarks>
static member Named name =
Field.Where name (Equal "")
/// Specify the name of the parameter for this field
/// <summary>Specify the name of the parameter for this field</summary>
/// <param name="name">The parameter name (including <tt>:</tt> or <tt>@</tt>)</param>
/// <returns>A field with the given parameter name specified</returns>
member this.WithParameterName name =
{ this with ParameterName = Some name }
/// Specify a qualifier (alias) for the table from which this field will be referenced
/// <summary>Specify a qualifier (alias) for the table from which this field will be referenced</summary>
/// <param name="alias">The table alias for this field comparison</param>
/// <returns>A field with the given qualifier specified</returns>
member this.WithQualifier alias =
{ this with Qualifier = Some alias }
/// Get the qualified path to the field
/// <summary>Get the qualified path to the field</summary>
/// <param name="dialect">The SQL dialect to use when converting the name to nested path format</param>
/// <param name="format">Whether to reference this path as a JSON value or a SQL value</param>
/// <returns>A <tt>string</tt> with the qualified path required to address the nested document value</returns>
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
/// <summary>How fields should be matched</summary>
[<Struct>]
type FieldMatch =
/// Any field matches (OR)
/// <summary>Any field matches (<tt>OR</tt>)</summary>
| Any
/// All fields match (AND)
/// <summary>All fields match (<tt>AND</tt>)</summary>
| All
/// The SQL value implementing each matching strategy
/// <summary>The SQL value implementing each matching strategy</summary>
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
/// <summary>Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)</summary>
type ParameterName() =
/// The counter for the next field value
let mutable currentIdx = -1
/// <summary>
/// Return the specified name for the parameter, or an anonymous parameter name if none is specified
/// </summary>
/// <param name="paramName">The optional name of the parameter</param>
/// <returns>The name of the parameter, derived if no name was provided</returns>
member this.Derive paramName =
match paramName with
| Some it -> it
@ -231,31 +316,41 @@ type ParameterName() =
$"@field{currentIdx}"
/// Automatically-generated document ID strategies
/// <summary>Automatically-generated document ID strategies</summary>
[<Struct>]
type AutoId =
/// No automatic IDs will be generated
/// <summary>No automatic IDs will be generated</summary>
| Disabled
/// Generate a MAX-plus-1 numeric value for documents
/// <summary>Generate a MAX-plus-1 numeric value for documents</summary>
| Number
/// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
/// <summary>Generate a <tt>GUID</tt> for each document (as a lowercase, no-dashes, 32-character string)</summary>
| Guid
/// Generate a random string of hexadecimal characters for each document
/// <summary>Generate a random string of hexadecimal characters for each document</summary>
| RandomString
with
/// Generate a GUID string
/// <summary>Generate a <tt>GUID</tt> string</summary>
/// <returns>A <tt>GUID</tt> string</returns>
static member GenerateGuid() =
System.Guid.NewGuid().ToString "N"
/// Generate a string of random hexadecimal characters
/// <summary>Generate a string of random hexadecimal characters</summary>
/// <param name="length">The number of characters to generate</param>
/// <returns>A string of the given length with random hexadecimal characters</returns>
static member GenerateRandomString(length: int) =
RandomNumberGenerator.GetHexString(length, lowercase = true)
/// Does the given document need an automatic ID generated?
/// <summary>Does the given document need an automatic ID generated?</summary>
/// <param name="strategy">The auto-ID strategy currently in use</param>
/// <param name="document">The document being inserted</param>
/// <param name="idProp">The name of the ID property in the given document</param>
/// <returns>True if an auto-ID strategy is implemented and the ID has no value, false otherwise</returns>
/// <exception cref="T:System.InvalidOperationException">
/// If the ID field type and requested ID value are not compatible
/// </exception>
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
/// <summary>The required document serialization implementation</summary>
type IDocumentSerializer =
/// Serialize an object to a JSON string
/// <summary>Serialize an object to a JSON string</summary>
abstract Serialize<'T> : 'T -> string
/// Deserialize a JSON string into an object
/// <summary>Deserialize a JSON string into an object</summary>
abstract Deserialize<'T> : string -> 'T
/// Document serializer defaults
/// <summary>Document serializer defaults</summary>
module DocumentSerializer =
open System.Text.Json
@ -312,7 +407,7 @@ module DocumentSerializer =
o.Converters.Add(JsonFSharpConverter())
o
/// The default JSON serializer
/// <summary>The default JSON serializer</summary>
[<CompiledName "Default">]
let ``default`` =
{ new IDocumentSerializer with
@ -323,19 +418,21 @@ module DocumentSerializer =
}
/// Configuration for document handling
/// <summary>Configuration for document handling</summary>
[<RequireQualifiedAccess>]
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
/// <summary>Register a serializer to use for translating documents to domain types</summary>
/// <param name="ser">The serializer to use when manipulating documents</param>
[<CompiledName "UseSerializer">]
let useSerializer ser =
serializerValue <- ser
/// Retrieve the currently configured serializer
/// <summary>Retrieve the currently configured serializer</summary>
/// <returns>The currently configured serializer</returns>
[<CompiledName "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
/// <summary>Specify the name of the ID field for documents</summary>
/// <param name="it">The name of the ID field for documents</param>
[<CompiledName "UseIdField">]
let useIdField it =
idFieldValue <- it
/// Retrieve the currently configured ID field for documents
/// <summary>Retrieve the currently configured ID field for documents</summary>
/// <returns>The currently configured ID field</returns>
[<CompiledName "IdField">]
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
/// <summary>Specify the automatic ID generation strategy used by the library</summary>
/// <param name="it">The automatic ID generation strategy to use</param>
[<CompiledName "UseAutoIdStrategy">]
let useAutoIdStrategy it =
autoIdValue <- it
/// Retrieve the currently configured automatic ID generation strategy
/// <summary>Retrieve the currently configured automatic ID generation strategy</summary>
/// <returns>The current automatic ID generation strategy</returns>
[<CompiledName "AutoIdStrategy">]
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
/// <summary>Specify the length of automatically generated random strings</summary>
/// <param name="length">The length of automatically generated random strings</param>
[<CompiledName "UseIdStringLength">]
let useIdStringLength length =
idStringLengthValue <- length
/// Retrieve the currently configured length of automatically generated random strings
/// <summary>Retrieve the currently configured length of automatically generated random strings</summary>
/// <returns>The current length of automatically generated random strings</returns>
[<CompiledName "IdStringLength">]
let idStringLength () =
idStringLengthValue
/// Query construction functions
/// <summary>Query construction functions</summary>
[<RequireQualifiedAccess>]
module Query =
/// Combine a query (select, update, etc.) and a WHERE clause
/// <summary>Combine a query (<tt>SELECT</tt>, <tt>UPDATE</tt>, etc.) and a <tt>WHERE</tt> clause</summary>
/// <param name="statement">The first part of the statement</param>
/// <param name="where">The <tt>WHERE</tt> clause for the statement</param>
/// <returns>The two parts of the query combined with <tt>WHERE</tt></returns>
[<CompiledName "StatementWhere">]
let statementWhere statement where =
$"%s{statement} WHERE %s{where}"
/// Queries to define tables and indexes
/// <summary>Queries to define tables and indexes</summary>
module Definition =
/// SQL statement to create a document table
/// <summary>SQL statement to create a document table</summary>
/// <param name="name">The name of the table to create (may include schema)</param>
/// <param name="dataType">The type of data for the column (<tt>JSON</tt>, <tt>JSONB</tt>, etc.)</param>
/// <returns>A query to create a document table</returns>
[<CompiledName "EnsureTableFor">]
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
/// <summary>SQL statement to create an index on one or more fields in a JSON document</summary>
/// <param name="tableName">The table on which an index should be created (may include schema)</param>
/// <param name="indexName">The name of the index to be created</param>
/// <param name="fields">One or more fields to include in the index</param>
/// <param name="dialect">The SQL dialect to use when creating this index</param>
/// <returns>A query to create the field index</returns>
[<CompiledName "EnsureIndexOn">]
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
/// <summary>SQL statement to create a key index for a document table</summary>
/// <param name="tableName">The table on which a key index should be created (may include schema)</param>
/// <param name="dialect">The SQL dialect to use when creating this index</param>
/// <returns>A query to create the key index</returns>
[<CompiledName "EnsureKey">]
let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
/// Query to insert a document
/// <summary>Query to insert a document</summary>
/// <param name="tableName">The table into which to insert (may include schema)</param>
/// <returns>A query to insert a document</returns>
[<CompiledName "Insert">]
let insert tableName =
$"INSERT INTO %s{tableName} VALUES (@data)"
/// <summary>
/// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
/// </summary>
/// <param name="tableName">The table into which to save (may include schema)</param>
/// <returns>A query to save a document</returns>
[<CompiledName "Save">]
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)
/// <summary>Query to count documents in a table</summary>
/// <param name="tableName">The table in which to count documents (may include schema)</param>
/// <returns>A query to count documents</returns>
/// <remarks>This query has no <tt>WHERE</tt> clause</remarks>
[<CompiledName "Count">]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to check for document existence in a table
/// <summary>Query to check for document existence in a table</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="where">The <tt>WHERE</tt> clause with the existence criteria</param>
/// <returns>A query to check document existence</returns>
[<CompiledName "Exists">]
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)
/// <summary>Query to select documents from a table</summary>
/// <param name="tableName">The table from which documents should be found (may include schema)</param>
/// <returns>A query to retrieve documents</returns>
/// <remarks>This query has no <tt>WHERE</tt> clause</remarks>
[<CompiledName "Find">]
let find tableName =
$"SELECT data FROM %s{tableName}"
/// Query to update a document (no WHERE clause)
/// <summary>Query to update (replace) a document</summary>
/// <param name="tableName">The table in which documents should be replaced (may include schema)</param>
/// <returns>A query to update documents</returns>
/// <remarks>This query has no <tt>WHERE</tt> clause</remarks>
[<CompiledName "Update">]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
/// Query to delete documents from a table (no WHERE clause)
/// <summary>Query to delete documents from a table</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <returns>A query to delete documents</returns>
/// <remarks>This query has no <tt>WHERE</tt> clause</remarks>
[<CompiledName "Delete">]
let delete tableName =
$"DELETE FROM %s{tableName}"
/// Create a SELECT clause to retrieve the document data from the given table
/// <summary>Create a SELECT clause to retrieve the document data from the given table</summary>
/// <param name="tableName">The table from which documents should be found (may include schema)</param>
/// <returns>A query to retrieve documents</returns>
[<CompiledName "SelectFromTable">]
[<System.Obsolete "Use Find instead">]
let selectFromTable tableName =
find tableName
/// Create an ORDER BY clause for the given fields
/// <summary>Create an <tt>ORDER BY</tt> clause for the given fields</summary>
/// <param name="fields">One or more fields by which to order</param>
/// <param name="dialect">The SQL dialect for the generated clause</param>
/// <returns>An <tt>ORDER BY</tt> clause for the given fields</returns>
[<CompiledName "OrderBy">]
let orderBy fields dialect =
if Seq.isEmpty fields then ""

View File

@ -1,31 +1,36 @@
namespace BitBadger.Documents.Postgres
/// The type of index to generate for the document
/// <summary>The type of index to generate for the document</summary>
[<Struct>]
type DocumentIndex =
/// A GIN index with standard operations (all operators supported)
/// <summary>A <tt>GIN</tt> index with standard operations (all operators supported)</summary>
| Full
/// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators)
/// <summary>
/// A <tt>GIN</tt> index with JSONPath operations (optimized for <tt>@&gt;</tt>, <tt>@?</tt>, <tt>@@</tt> operators)
/// </summary>
| Optimized
open Npgsql
/// Configuration for document handling
/// <summary>Configuration for document handling</summary>
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)
/// <summary>Register a data source to use for query execution (disposes the current one if it exists)</summary>
/// <param name="source">The data source to use</param>
[<CompiledName "UseDataSource">]
let useDataSource source =
if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose()
dataSourceValue <- Some source
/// Retrieve the currently configured data source
/// <summary>Retrieve the currently configured data source</summary>
/// <returns>The current data source</returns>
/// <exception cref="T:System.InvalidOperationException">If no data source has been configured</exception>
[<CompiledName "DataSource">]
let dataSource () =
match dataSourceValue with
@ -69,21 +74,29 @@ module private Helpers =
open BitBadger.Documents
/// Functions for creating parameters
/// <summary>Functions for creating parameters</summary>
[<AutoOpen>]
module Parameters =
/// Create an ID parameter (name "@id")
/// <summary>Create an ID parameter (name "@id")</summary>
/// <param name="key">The key value for the ID parameter</param>
/// <returns>The name and parameter value for the ID</returns>
[<CompiledName "Id">]
let idParam (key: 'TKey) =
"@id", parameterFor key (fun it -> Sql.string (string it))
/// Create a parameter with a JSON value
/// <summary>Create a parameter with a JSON value</summary>
/// <param name="name">The name of the parameter to create</param>
/// <param name="it">The criteria to provide as JSON</param>
/// <returns>The name and parameter value for the JSON field</returns>
[<CompiledName "Json">]
let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create JSON field parameters
/// <summary>Create JSON field parameters</summary>
/// <param name="fields">The <tt>Field</tt>s to convert to parameters</param>
/// <param name="parameters">The current parameters for the query</param>
/// <returns>A unified sequence of parameter names and values</returns>
[<CompiledName "AddFields">]
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
/// <summary>Append JSON field name parameters for the given field names to the given parameters</summary>
/// <param name="fieldNames">The names of fields to be addressed</param>
/// <returns>The name (<tt>@name</tt>) and parameter value for the field names</returns>
[<CompiledName "FieldNames">]
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
/// <summary>An empty parameter sequence</summary>
[<CompiledName "None">]
let noParams =
Seq.empty<string * SqlValue>
/// Query construction functions
/// <summary>Query construction functions</summary>
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
/// <summary>
/// Create a <tt>WHERE</tt> clause fragment to implement a comparison on fields in a JSON document
/// </summary>
/// <param name="howMatched">How the fields should be matched</param>
/// <param name="fields">The fields for the comparisons</param>
/// <returns>A <tt>WHERE</tt> clause implementing the comparisons for the given fields</returns>
[<CompiledName "WhereByFields">]
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
/// <summary>Create a <tt>WHERE</tt> clause fragment to implement an ID-based query</summary>
/// <param name="docId">The ID of the document</param>
/// <returns>A <tt>WHERE</tt> clause fragment identifying a document by its ID</returns>
[<CompiledName "WhereById">]
let whereById<'TKey> (docId: 'TKey) =
whereByFields Any [ { Field.Equal (Configuration.idField ()) docId with ParameterName = Some "@id" } ]
/// Table and index definition queries
/// <summary>Table and index definition queries</summary>
module Definition =
/// SQL statement to create a document table
/// <summary>SQL statement to create a document table</summary>
/// <param name="name">The name of the table (may include schema)</param>
/// <returns>A query to create the table if it does not exist</returns>
[<CompiledName "EnsureTable">]
let ensureTable name =
Query.Definition.ensureTableFor name "JSONB"
/// SQL statement to create an index on JSON documents in the specified table
/// <summary>SQL statement to create an index on JSON documents in the specified table</summary>
/// <param name="name">The name of the table to be indexed (may include schema)</param>
/// <param name="idxType">The type of document index to create</param>
/// <returns>A query to create the index if it does not exist</returns>
[<CompiledName "EnsureDocumentIndex">]
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
/// <summary>
/// Create a <tt>WHERE</tt> clause fragment to implement a <tt>@&gt;</tt> (JSON contains) condition
/// </summary>
/// <param name="paramName">The parameter name for the query</param>
/// <returns>A <tt>WHERE</tt> clause fragment for the contains condition</returns>
[<CompiledName "WhereDataContains">]
let whereDataContains paramName =
$"data @> %s{paramName}"