26 Commits

Author SHA1 Message Date
88f11a854f Add compat namespace functions
- Update package release notes
2024-08-17 14:08:46 -04:00
e2948ea6a1 Add ordered* tests for SQLite
- Ignore "n:" prefix for SQLite fields
- Remove byFIeld functions
2024-08-17 12:38:14 -04:00
d993f71788 Reorg SQLite tests 2024-08-16 22:54:27 -04:00
fba8ec7bc5 Finish ordered tests for PostgreSQL
- Rename tests, remove tested obsolete byField funcs
2024-08-16 22:04:23 -04:00
8e33299e22 WIP on ordered tests, test reorg 2024-08-15 22:32:25 -04:00
e07844570a Fix numeric field rendering in order by 2024-08-15 19:16:00 -04:00
bac3bd2ef0 Add *Ordered functions
- WIP on PostgreSQL numeric ordering
2024-08-14 22:13:46 -04:00
edd4d006da First cut of field order by function 2024-08-13 09:03:05 -04:00
96f5e1515d Remove byField functions 2024-08-12 09:07:25 -04:00
d382699a17 Merge branch 'wip-1' of https://git.bitbadger.solutions/bit-badger/BitBadger.Documents into wip-1 2024-08-11 17:06:07 -04:00
0c308c5f33 Add ToString for FieldMatch; add tests 2024-08-11 17:05:58 -04:00
77a6aaa583 Ignore IDE settings 2024-08-11 12:10:07 -04:00
d9d37f110d Add tests for PG parameter types
- Alter whereById to expect a parameter
2024-08-10 19:51:13 -04:00
e0fb793ec9 WIP on Postgres type derivation 2024-08-10 15:07:59 -04:00
74e5b77edb Complete common migration, WIP on tests 2024-08-10 09:56:59 -04:00
98bc83ac17 Pull count, exists, update to common 2024-08-09 22:55:12 -04:00
35755df99a WIP on pulling code up to Common 2024-08-09 20:15:08 -04:00
b1c3991e11 WIP on supporting numeric fields 2024-08-09 18:42:10 -04:00
85750e19f2 Add Postgres Query byFields tests 2024-08-08 22:36:48 -04:00
d8f64417e5 Add byFields to SQLite implementation
- Add fieldNameParams/FieldNames to Postgres
(syncs names between both implementations)
2024-08-08 19:45:25 -04:00
202fca272e Prefer seq over list for parameters
- This greatly reduces the duplicated functions needed
between F# and C#; F# lists satisfy the sequence reqs
2024-08-08 17:32:44 -04:00
8a15bdce2e Add byFields to PostgreSQL impl
- Swap howMatched and fields throughout
- Existing tests pass; still need new ones for byFields
2024-08-07 23:23:28 -04:00
d131eda56e Remove internal calls to deprecated funcs 2024-08-07 20:20:10 -04:00
e2232e91bb Add whereByFields code / tests
- Add wrapper class for unnamed field parameters
- Support table qualifiers by field
- Support dot access to document fields/sub-fields
2024-08-07 16:39:15 -04:00
e96c449324 WIP on PG Query.whereByFIelds 2024-08-07 08:30:28 -04:00
433302d995 WIP on field enhancements 2024-08-04 18:59:32 -04:00
33 changed files with 2790 additions and 5717 deletions

3
.gitignore vendored
View File

@@ -397,6 +397,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
**/.idea
# Test run files
src/*-tests.txt

View File

@@ -3,7 +3,6 @@
<PropertyGroup>
<Description>Common files for PostgreSQL and SQLite document database libraries</Description>
<PackageTags>JSON Document SQL</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -14,9 +13,7 @@
<ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="System.Text.Encodings.Web" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

View File

@@ -1,313 +1,146 @@
namespace BitBadger.Documents
open System.Security.Cryptography
/// The types of logical operations available for JSON fields
[<Struct>]
type Op =
/// Equals (=)
| EQ
/// Greater Than (>)
| GT
/// Greater Than or Equal To (>=)
| GE
/// Less Than (<)
| LT
/// Less Than or Equal To (<=)
| LE
/// Not Equal to (<>)
| NE
/// Between (BETWEEN)
| BT
/// Exists (IS NOT NULL)
| EX
/// Does Not Exist (IS NULL)
| NEX
/// <summary>The types of comparisons available for JSON fields</summary>
/// <exclude />
type Comparison =
/// <summary>Equals (<tt>=</tt>)</summary>
| Equal of Value: obj
/// <summary>Greater Than (<tt>&gt;</tt>)</summary>
| Greater of Value: obj
/// <summary>Greater Than or Equal To (<tt>&gt;=</tt>)</summary>
| GreaterOrEqual of Value: obj
/// <summary>Less Than (<tt>&lt;</tt>)</summary>
| Less of Value: obj
/// <summary>Less Than or Equal To (<tt>&lt;=</tt>)</summary>
| LessOrEqual of Value: obj
/// <summary>Not Equal to (<tt>&lt;&gt;</tt>)</summary>
| NotEqual of Value: obj
/// <summary>Between (<tt>BETWEEN</tt>)</summary>
| Between of Min: obj * Max: obj
/// <summary>In (<tt>IN</tt>)</summary>
| In of Values: obj seq
/// <summary>In Array (PostgreSQL: <tt>|?</tt>, SQLite: <tt>EXISTS / json_each / IN</tt>)</summary>
| InArray of Table: string * Values: obj seq
/// <summary>Exists (<tt>IS NOT NULL</tt>)</summary>
| Exists
/// <summary>Does Not Exist (<tt>IS NULL</tt>)</summary>
| NotExists
/// <summary>The operator SQL for this comparison</summary>
member this.OpSql =
override this.ToString() =
match this with
| Equal _ -> "="
| Greater _ -> ">"
| GreaterOrEqual _ -> ">="
| Less _ -> "<"
| LessOrEqual _ -> "<="
| NotEqual _ -> "<>"
| Between _ -> "BETWEEN"
| In _ -> "IN"
| InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
| Exists -> "IS NOT NULL"
| NotExists -> "IS NULL"
| EQ -> "="
| GT -> ">"
| GE -> ">="
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
/// <summary>The dialect in which a command should be rendered</summary>
/// The dialect in which a command should be rendered
[<Struct>]
type Dialect =
| PostgreSQL
| SQLite
/// <summary>The format in which an element of a JSON field should be extracted</summary>
[<Struct>]
type FieldFormat =
/// <summary>
/// Use <tt>-&gt;&gt;</tt> or <tt>#&gt;&gt;</tt>; extracts a text (PostgreSQL) or SQL (SQLite) value
/// </summary>
| AsSql
/// <summary>Use <tt>-&gt;</tt> or <tt>#&gt;</tt>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value</summary>
| AsJson
/// <summary>Criteria for a field <tt>WHERE</tt> clause</summary>
/// Criteria for a field WHERE clause
type Field = {
/// <summary>The name of the field</summary>
/// The name of the field
Name: string
/// <summary>The comparison for the field</summary>
Comparison: Comparison
/// The operation by which the field will be compared
Op: Op
/// <summary>The name of the parameter for this field</summary>
/// The value of the field
Value: obj
/// The name of the parameter for this field
ParameterName: string option
/// <summary>The table qualifier for this field</summary>
/// The table qualifier for this field
Qualifier: string option
} with
/// <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
static member EQ name (value: obj) =
{ Name = name; Op = EQ; Value = value; ParameterName = None; Qualifier = None }
/// <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 a greater than (>) field criterion
static member GT name (value: obj) =
{ Name = name; Op = GT; Value = value; ParameterName = None; Qualifier = None }
/// <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 or equal to (>=) field criterion
static member GE name (value: obj) =
{ Name = name; Op = GE; Value = value; ParameterName = None; Qualifier = None }
/// <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 less than (<) field criterion
static member LT name (value: obj) =
{ Name = name; Op = LT; Value = value; ParameterName = None; Qualifier = None }
/// <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 less than or equal to (<=) field criterion
static member LE name (value: obj) =
{ Name = name; Op = LE; Value = value; ParameterName = None; Qualifier = None }
/// <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 not equals (<>) field criterion
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value; ParameterName = None; Qualifier = None }
/// <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 BETWEEN field criterion
static member BT name (min: obj) (max: obj) =
{ Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None }
/// <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 an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None }
/// <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 not exists (IS NULL) field criterion
static member NEX name =
{ Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None }
/// <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)
/// <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
/// <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)
/// <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
/// <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))
/// <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
/// <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))
/// <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
/// <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))
/// <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
/// <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
/// <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
/// <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
/// <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 =
/// Transform a field name (a.b.c) to a path for the given SQL dialect
static member NameToPath (name: string) dialect =
let path =
if name.Contains '.' then
match dialect with
| PostgreSQL ->
(match format with AsJson -> "#>" | AsSql -> "#>>")
+ "'{" + String.concat "," (name.Split '.') + "}'"
| SQLite ->
let parts = name.Split '.'
let last = Array.last parts
let final = (match format with AsJson -> "'->'" | AsSql -> "'->>'") + $"{last}'"
"->'" + String.concat "'->'" (Array.truncate (Array.length parts - 1) parts) + final
else
match format with AsJson -> $"->'{name}'" | AsSql -> $"->>'{name}'"
| PostgreSQL -> "#>>'{" + String.concat "," (name.Split '.') + "}'"
| SQLite -> "->>'" + String.concat "'->>'" (name.Split '.') + "'"
else $"->>'{name}'"
$"data{path}"
/// <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>
/// Create a field with a given name, but no other properties filled (op will be EQ, value will be "")
static member Named name =
Field.Where name (Equal "")
{ Name = name; Op = EQ; Value = ""; ParameterName = None; Qualifier = None }
/// <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>
/// Specify the name of the parameter for this field
member this.WithParameterName name =
{ this with ParameterName = Some name }
/// <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>
/// Specify a qualifier (alias) for the table from which this field will be referenced
member this.WithQualifier alias =
{ this with Qualifier = Some alias }
/// <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
/// Get the qualified path to the field
member this.Path dialect =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect
/// <summary>How fields should be matched</summary>
/// How fields should be matched
[<Struct>]
type FieldMatch =
/// <summary>Any field matches (<tt>OR</tt>)</summary>
/// Any field matches (OR)
| Any
/// <summary>All fields match (<tt>AND</tt>)</summary>
/// All fields match (AND)
| All
/// <summary>The SQL value implementing each matching strategy</summary>
/// The SQL value implementing each matching strategy
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// <summary>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)
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
@@ -316,86 +149,17 @@ type ParameterName() =
$"@field{currentIdx}"
/// <summary>Automatically-generated document ID strategies</summary>
[<Struct>]
type AutoId =
/// <summary>No automatic IDs will be generated</summary>
| Disabled
/// <summary>Generate a MAX-plus-1 numeric value for documents</summary>
| Number
/// <summary>Generate a <tt>GUID</tt> for each document (as a lowercase, no-dashes, 32-character string)</summary>
| Guid
/// <summary>Generate a random string of hexadecimal characters for each document</summary>
| RandomString
with
/// <summary>Generate a <tt>GUID</tt> string</summary>
/// <returns>A <tt>GUID</tt> string</returns>
static member GenerateGuid() =
System.Guid.NewGuid().ToString "N"
/// <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)
/// <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
| _ ->
let prop = document.GetType().GetProperty idProp
if isNull prop then invalidOp $"{idProp} not found in document"
else
match strategy with
| Number ->
if prop.PropertyType = typeof<int8> then
let value = prop.GetValue document :?> int8
value = int8 0
elif prop.PropertyType = typeof<int16> then
let value = prop.GetValue document :?> int16
value = int16 0
elif prop.PropertyType = typeof<int> then
let value = prop.GetValue document :?> int
value = 0
elif prop.PropertyType = typeof<int64> then
let value = prop.GetValue document :?> int64
value = int64 0
else invalidOp "Document ID was not a number; cannot auto-generate a Number ID"
| Guid | RandomString ->
if prop.PropertyType = typeof<string> then
let value =
prop.GetValue document
|> Option.ofObj
|> Option.map (fun it -> it :?> string)
|> Option.defaultValue ""
value = ""
else invalidOp "Document ID was not a string; cannot auto-generate GUID or random string"
| Disabled -> false
/// <summary>The required document serialization implementation</summary>
/// The required document serialization implementation
type IDocumentSerializer =
/// <summary>Serialize an object to a JSON string</summary>
/// Serialize an object to a JSON string
abstract Serialize<'T> : 'T -> string
/// <summary>Deserialize a JSON string into an object</summary>
/// Deserialize a JSON string into an object
abstract Deserialize<'T> : string -> 'T
/// <summary>Document serializer defaults</summary>
/// Document serializer defaults
module DocumentSerializer =
open System.Text.Json
@@ -407,7 +171,7 @@ module DocumentSerializer =
o.Converters.Add(JsonFSharpConverter())
o
/// <summary>The default JSON serializer</summary>
/// The default JSON serializer
[<CompiledName "Default">]
let ``default`` =
{ new IDocumentSerializer with
@@ -418,90 +182,50 @@ module DocumentSerializer =
}
/// <summary>Configuration for document handling</summary>
/// Configuration for document handling
[<RequireQualifiedAccess>]
module Configuration =
/// The serializer to use for document manipulation
let mutable private serializerValue = DocumentSerializer.``default``
/// <summary>Register a serializer to use for translating documents to domain types</summary>
/// <param name="ser">The serializer to use when manipulating documents</param>
/// Register a serializer to use for translating documents to domain types
[<CompiledName "UseSerializer">]
let useSerializer ser =
serializerValue <- ser
/// <summary>Retrieve the currently configured serializer</summary>
/// <returns>The currently configured serializer</returns>
/// Retrieve the currently configured serializer
[<CompiledName "Serializer">]
let serializer () =
serializerValue
/// The serialized name of the ID field for documents
let mutable private idFieldValue = "Id"
let mutable idFieldValue = "Id"
/// <summary>Specify the name of the ID field for documents</summary>
/// <param name="it">The name of the ID field for documents</param>
/// Specify the name of the ID field for documents
[<CompiledName "UseIdField">]
let useIdField it =
idFieldValue <- it
/// <summary>Retrieve the currently configured ID field for documents</summary>
/// <returns>The currently configured ID field</returns>
/// Retrieve the currently configured ID field for documents
[<CompiledName "IdField">]
let idField () =
idFieldValue
/// The automatic ID strategy used by the library
let mutable private autoIdValue = Disabled
/// <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
/// <summary>Retrieve the currently configured automatic ID generation strategy</summary>
/// <returns>The current automatic ID generation strategy</returns>
[<CompiledName "AutoIdStrategy">]
let autoIdStrategy () =
autoIdValue
/// The length of automatically generated random strings
let mutable private idStringLengthValue = 16
/// <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
/// <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
/// <summary>Query construction functions</summary>
/// Query construction functions
[<RequireQualifiedAccess>]
module Query =
/// <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>
/// Combine a query (select, update, etc.) and a WHERE clause
[<CompiledName "StatementWhere">]
let statementWhere statement where =
$"%s{statement} WHERE %s{where}"
/// <summary>Queries to define tables and indexes</summary>
/// Queries to define tables and indexes
module Definition =
/// <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>
/// SQL statement to create a document table
[<CompiledName "EnsureTableFor">]
let ensureTableFor name dataType =
$"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)"
@@ -511,12 +235,7 @@ module Query =
let parts = tableName.Split '.'
if Array.length parts = 1 then "", tableName else parts[0], parts[1]
/// <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>
/// SQL statement to create an index on one or more fields in a JSON document
[<CompiledName "EnsureIndexOn">]
let ensureIndexOn tableName indexName (fields: string seq) dialect =
let _, tbl = splitSchemaAndTable tableName
@@ -526,88 +245,59 @@ module Query =
let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"({Field.NameToPath fieldName dialect AsSql}){direction}")
$"({Field.NameToPath fieldName dialect}){direction}")
|> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
/// <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>
/// SQL statement to create a key index for a document table
[<CompiledName "EnsureKey">]
let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
/// <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>
/// Query to insert a document
[<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 ())
/// <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>
/// Query to count documents in a table (no WHERE clause)
[<CompiledName "Count">]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// <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>
/// Query to check for document existence in a table
[<CompiledName "Exists">]
let exists tableName where =
$"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it"
/// <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>
/// Query to select documents from a table (no WHERE clause)
[<CompiledName "Find">]
let find tableName =
$"SELECT data FROM %s{tableName}"
/// <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>
/// Query to update a document (no WHERE clause)
[<CompiledName "Update">]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
/// <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>
/// Query to delete documents from a table (no WHERE clause)
[<CompiledName "Delete">]
let delete tableName =
$"DELETE FROM %s{tableName}"
/// <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>
/// Create a SELECT clause to retrieve the document data from the given table
[<CompiledName "SelectFromTable">]
[<System.Obsolete "Use Find instead">]
let selectFromTable tableName =
find tableName
/// <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>
/// Create an ORDER BY clause for the given fields
[<CompiledName "OrderBy">]
let orderBy fields dialect =
if Seq.isEmpty fields then ""
@@ -616,18 +306,13 @@ module Query =
|> Seq.map (fun it ->
if it.Name.Contains ' ' then
let parts = it.Name.Split ' '
{ it with Name = parts[0] }, Some $""" {parts |> Array.skip 1 |> String.concat " "}"""
{ it with Name = parts[0] }, Some $" {parts[1]}"
else it, None)
|> Seq.map (fun (field, direction) ->
if field.Name.StartsWith "n:" then
let f = { field with Name = field.Name[2..] }
match dialect with
| PostgreSQL -> $"({f.Path PostgreSQL AsSql})::numeric"
| SQLite -> f.Path SQLite AsSql
elif field.Name.StartsWith "i:" then
let p = { field with Name = field.Name[2..] }.Path dialect AsSql
match dialect with PostgreSQL -> $"LOWER({p})" | SQLite -> $"{p} COLLATE NOCASE"
else field.Path dialect AsSql
match dialect, field.Name.StartsWith "n:" with
| PostgreSQL, true -> $"({ { field with Name = field.Name[2..] }.Path PostgreSQL})::numeric"
| SQLite, true -> { field with Name = field.Name[2..] }.Path SQLite
| _, _ -> field.Path dialect
|> function path -> path + defaultArg direction "")
|> String.concat ", "
|> function it -> $" ORDER BY {it}"

View File

@@ -7,7 +7,6 @@ This package provides common definitions and functionality for `BitBadger.Docume
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries)
- Accesses documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Uses `Task`-based async for all data access functions

View File

@@ -1,13 +1,12 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyVersion>4.0.1.0</AssemblyVersion>
<FileVersion>4.0.1.0</FileVersion>
<VersionPrefix>4.0.1</VersionPrefix>
<PackageReleaseNotes>From v4.0: Add XML documention (IDE support)
From v3.1: See 4.0 release for breaking changes and compatibility</PackageReleaseNotes>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion>
<VersionPrefix>4.0.0</VersionPrefix>
<PackageReleaseNotes>Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile>

View File

@@ -3,13 +3,10 @@
<PropertyGroup>
<Description>Use PostgreSQL as a document database</Description>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<Compile Include="WithProps.fs" />
<Compile Include="Functions.fs" />
<Compile Include="Extensions.fs" />
<Compile Include="Compat.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
@@ -17,9 +14,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="9.0.2" />
<PackageReference Include="Npgsql.FSharp" Version="8.0.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -71,7 +71,7 @@ module WithProps =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, sqlProps) =
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
[<RequireQualifiedAccess>]
@@ -144,7 +144,7 @@ module Find =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
@@ -248,7 +248,7 @@ type NpgsqlConnectionCSharpCompatExtensions =
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) =
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)

File diff suppressed because it is too large Load Diff

View File

@@ -1,617 +0,0 @@
namespace BitBadger.Documents.Postgres
/// <summary>Commands to execute custom SQL queries</summary>
[<RequireQualifiedAccess>]
module Custom =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.list<'TDoc> query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns><tt>Some</tt> with the first matching result, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>The first matching result, or <tt>null</tt> if not found</returns>
let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
[<CompiledName "NonQuery">]
let nonQuery query parameters =
WithProps.Custom.nonQuery query parameters (fromDataSource ())
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) =
WithProps.Custom.scalar query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>) =
WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ())
/// <summary>Table and index definition commands</summary>
[<RequireQualifiedAccess>]
module Definition =
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
[<CompiledName "EnsureTable">]
let ensureTable name =
WithProps.Definition.ensureTable name (fromDataSource ())
/// <summary>Create an index on documents in the specified table</summary>
/// <param name="name">The table to be indexed (may include schema)</param>
/// <param name="idxType">The type of document index to create</param>
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex name idxType =
WithProps.Definition.ensureDocumentIndex name idxType (fromDataSource ())
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
WithProps.Definition.ensureFieldIndex tableName indexName fields (fromDataSource ())
/// <summary>Document writing functions</summary>
[<AutoOpen>]
module Document =
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.insert<'TDoc> tableName document (fromDataSource ())
/// <summary>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 the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.save<'TDoc> tableName document (fromDataSource ())
/// <summary>Queries to count documents</summary>
[<RequireQualifiedAccess>]
module Count =
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "All">]
let all tableName =
WithProps.Count.all tableName (fromDataSource ())
/// <summary>Count matching documents using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Count.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Count matching documents using a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Count.byContains tableName criteria (fromDataSource ())
/// <summary>Count matching documents using a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ())
/// <summary>Queries to determine if documents exist</summary>
[<RequireQualifiedAccess>]
module Exists =
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
[<CompiledName "ById">]
let byId tableName docId =
WithProps.Exists.byId tableName docId (fromDataSource ())
/// <summary>Determine if a document exists using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Exists.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Determine if a document exists using a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Exists.byContains tableName criteria (fromDataSource ())
/// <summary>Determine if a document exists using a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ())
/// <summary>Commands to retrieve documents</summary>
[<RequireQualifiedAccess>]
module Find =
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
WithProps.Find.all<'TDoc> tableName (fromDataSource ())
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
let All<'TDoc> tableName =
WithProps.Find.All<'TDoc>(tableName, fromDataSource ())
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields =
WithProps.Find.allOrdered<'TDoc> tableName orderFields (fromDataSource ())
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
let AllOrdered<'TDoc> tableName orderFields =
WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, fromDataSource ())
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns><tt>Some</tt> with the document if found, <tt>None</tt> otherwise</returns>
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ())
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The document if found, <tt>null</tt> otherwise</returns>
let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ())
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields =
WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
let ByFields<'TDoc>(tableName, howMatched, fields) =
WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "FSharpByFieldsOrdered">]
let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) =
WithProps.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ())
/// <summary>Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>All documents matching the given containment query</returns>
[<CompiledName "FSharpByContains">]
let byContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.byContains<'TDoc> tableName criteria (fromDataSource ())
/// <summary>Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>All documents matching the given containment query</returns>
let ByContains<'TDoc>(tableName, criteria: obj) =
WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
[<CompiledName "FSharpByContainsOrdered">]
let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields) =
WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ())
/// <summary>Retrieve documents matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>All documents matching the given JSON Path expression</returns>
[<CompiledName "FSharpByJsonPath">]
let byJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// <summary>Retrieve documents matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>All documents matching the given JSON Path expression</returns>
let ByJsonPath<'TDoc>(tableName, jsonPath) =
WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
[<CompiledName "FSharpByJsonPathOrdered">]
let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields) =
WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields) =
WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByContains">]
let firstByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ())
/// <summary>Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByContainsOrdered">]
let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, orderFields) =
WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// <summary>Retrieve the first document matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByJsonPathOrdered">]
let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, orderFields) =
WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ())
/// <summary>Commands to update documents</summary>
[<RequireQualifiedAccess>]
module Update =
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
WithProps.Update.byId tableName docId document (fromDataSource ())
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
[<CompiledName "FSharpFullFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithProps.Update.byFunc tableName idFunc document (fromDataSource ())
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
WithProps.Update.ByFunc(tableName, idFunc, document, fromDataSource ())
/// <summary>Commands to patch (partially update) documents</summary>
[<RequireQualifiedAccess>]
module Patch =
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
WithProps.Patch.byId tableName docId patch (fromDataSource ())
/// <summary>
/// Patch documents using a JSON field comparison query in the <tt>WHERE</tt> clause (<tt>-&gt;&gt; =</tt>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) =
WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ())
/// <summary>Patch documents using a JSON containment query in the <tt>WHERE</tt> clause (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
WithProps.Patch.byContains tableName criteria patch (fromDataSource ())
/// <summary>Patch documents using a JSON Path match query in the <tt>WHERE</tt> clause (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath (patch: 'TPatch) =
WithProps.Patch.byJsonPath tableName jsonPath patch (fromDataSource ())
/// <summary>Commands to remove fields from documents</summary>
[<RequireQualifiedAccess>]
module RemoveFields =
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames =
WithProps.RemoveFields.byId tableName docId fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) fieldNames =
WithProps.RemoveFields.byContains tableName criteria fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath fieldNames =
WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (fromDataSource ())
/// <summary>Commands to delete documents</summary>
[<RequireQualifiedAccess>]
module Delete =
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (fromDataSource ())
/// <summary>Delete documents by matching a JSON field comparison query (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Delete.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Delete documents by matching a JSON contains query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) =
WithProps.Delete.byContains tableName criteria (fromDataSource ())
/// <summary>Delete documents by matching a JSON Path match query (@?)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Delete.byJsonPath tableName jsonPath (fromDataSource ())

File diff suppressed because it is too large Load Diff

View File

@@ -5,16 +5,11 @@ This package provides a lightweight document library backed by [PostgreSQL](http
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries)
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions
- Use building blocks for more complex queries
## Upgrading from v3
There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details.
## Getting Started
Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library:
@@ -71,7 +66,7 @@ var customer = await Find.ById<string, Customer>("customer", "123");
// Find.byId type signature is string -> 'TKey -> Task<'TDoc option>
let! customer = Find.byId<string, Customer> "customer" "123"
```
_(keys are treated as strings or numbers depending on their defintion; however, they are indexed as strings)_
_(keys are treated as strings in the database)_
Count customers in Atlanta (using JSON containment):

View File

@@ -1,840 +0,0 @@
/// <summary>Versions of queries that accept <tt>SqlProps</tt> as the last parameter</summary>
module BitBadger.Documents.Postgres.WithProps
open BitBadger.Documents
open Npgsql.FSharp
/// <summary>Commands to execute custom SQL queries</summary>
[<RequireQualifiedAccess>]
module Custom =
module FSharpList = Microsoft.FSharp.Collections.List
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>A list of results for the given query</returns>
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps =
Sql.query query sqlProps
|> Sql.parameters (FSharpList.ofSeq parameters)
|> Sql.executeAsync mapFunc
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>A list of results for the given query</returns>
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! results = list<'TDoc> query parameters mapFunc.Invoke sqlProps
return ResizeArray results
}
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns><tt>Some</tt> with the first matching result, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters mapFunc sqlProps = backgroundTask {
let! results = list<'TDoc> query parameters mapFunc sqlProps
return FSharpList.tryHead results
}
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first matching result, or <tt>null</tt> if not found</returns>
let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps
return Option.toObj result
}
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "NonQuery">]
let nonQuery query parameters sqlProps =
Sql.query query sqlProps
|> Sql.parameters (FSharpList.ofSeq parameters)
|> Sql.executeNonQueryAsync
|> ignoreTask
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The scalar value for the query</returns>
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps =
Sql.query query sqlProps
|> Sql.parameters (FSharpList.ofSeq parameters)
|> Sql.executeRowAsync mapFunc
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The scalar value for the query</returns>
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) =
scalar<'T> query parameters mapFunc.Invoke sqlProps
/// <summary>Table and index definition commands</summary>
module Definition =
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "EnsureTable">]
let ensureTable name sqlProps = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps
do! Custom.nonQuery (Query.Definition.ensureKey name PostgreSQL) [] sqlProps
}
/// <summary>Create an index on documents in the specified table</summary>
/// <param name="name">The table to be indexed (may include schema)</param>
/// <param name="idxType">The type of document index to create</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex name idxType sqlProps =
Custom.nonQuery (Query.Definition.ensureDocumentIndex name idxType) [] sqlProps
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields sqlProps =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields PostgreSQL) [] sqlProps
/// <summary>Commands to add documents</summary>
[<AutoOpen>]
module Document =
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) sqlProps =
let query =
match Configuration.autoIdStrategy () with
| Disabled -> Query.insert tableName
| strategy ->
let idField = Configuration.idField ()
let dataParam =
if AutoId.NeedsAutoId strategy document idField then
match strategy with
| Number ->
$"' || (SELECT COALESCE(MAX((data->>'{idField}')::numeric), 0) + 1 FROM {tableName}) || '"
| Guid -> $"\"{AutoId.GenerateGuid()}\""
| RandomString -> $"\"{AutoId.GenerateRandomString(Configuration.idStringLength ())}\""
| Disabled -> "@data"
|> function it -> $"""@data::jsonb || ('{{"{idField}":{it}}}')::jsonb"""
else "@data"
(Query.insert tableName).Replace("@data", dataParam)
Custom.nonQuery query [ jsonParam "@data" document ] sqlProps
/// <summary>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 the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) sqlProps =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] sqlProps
/// <summary>Commands to count documents</summary>
[<RequireQualifiedAccess>]
module Count =
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "All">]
let all tableName sqlProps =
Custom.scalar (Query.count tableName) [] toCount sqlProps
/// <summary>Count matching documents using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The count of matching documents in the table</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.scalar
(Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount sqlProps
/// <summary>Count matching documents using a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) sqlProps =
Custom.scalar
(Query.byContains (Query.count tableName)) [ jsonParam "@criteria" criteria ] toCount sqlProps
/// <summary>Count matching documents using a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath sqlProps =
Custom.scalar
(Query.byPathMatch (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps
/// <summary>Commands to determine if documents exist</summary>
[<RequireQualifiedAccess>]
module Exists =
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>True if a document exists, false if not</returns>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) sqlProps =
Custom.scalar (Query.exists tableName (Query.whereById docId)) [ idParam docId ] toExists sqlProps
/// <summary>Determine if a document exists using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.scalar
(Query.exists tableName (Query.whereByFields howMatched fields))
(addFieldParams fields [])
toExists
sqlProps
/// <summary>Determine if a document exists using a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) sqlProps =
Custom.scalar
(Query.exists tableName (Query.whereDataContains "@criteria"))
[ jsonParam "@criteria" criteria ]
toExists
sqlProps
/// <summary>Determine if a document exists using a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath sqlProps =
Custom.scalar
(Query.exists tableName (Query.whereJsonPathMatches "@path"))
[ "@path", Sql.string jsonPath ]
toExists
sqlProps
/// <summary>Commands to retrieve documents</summary>
[<RequireQualifiedAccess>]
module Find =
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents from the given table</returns>
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName sqlProps =
Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> sqlProps
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents from the given table</returns>
let All<'TDoc>(tableName, sqlProps) =
Custom.List<'TDoc>(Query.find tableName, [], fromData<'TDoc>, sqlProps)
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields sqlProps =
Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields PostgreSQL) [] fromData<'TDoc> sqlProps
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
let AllOrdered<'TDoc>(tableName, orderFields, sqlProps) =
Custom.List<'TDoc>(
Query.find tableName + Query.orderBy orderFields PostgreSQL, [], fromData<'TDoc>, sqlProps)
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns><tt>Some</tt> with the document if found, <tt>None</tt> otherwise</returns>
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps =
Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The document if found, <tt>null</tt> otherwise</returns>
let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey, sqlProps) =
Custom.Single<'TDoc>(
Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps)
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given fields</returns>
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields sqlProps =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
fromData<'TDoc>
sqlProps
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given fields</returns>
let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched fields,
addFieldParams fields [],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "FSharpByFieldsOrdered">]
let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL)
(addFieldParams queryFields [])
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, sqlProps) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL,
addFieldParams queryFields [],
fromData<'TDoc>,
sqlProps)
/// <summary>Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given containment query</returns>
[<CompiledName "FSharpByContains">]
let byContains<'TDoc> tableName (criteria: obj) sqlProps =
Custom.list<'TDoc>
(Query.byContains (Query.find tableName)) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps
/// <summary>Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given containment query</returns>
let ByContains<'TDoc>(tableName, criteria: obj, sqlProps) =
Custom.List<'TDoc>(
Query.byContains (Query.find tableName),
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
[<CompiledName "FSharpByContainsOrdered">]
let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps =
Custom.list<'TDoc>
(Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL)
[ jsonParam "@criteria" criteria ]
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve documents matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields, sqlProps) =
Custom.List<'TDoc>(
Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL,
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
/// <summary>Retrieve documents matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given JSON Path expression</returns>
[<CompiledName "FSharpByJsonPath">]
let byJsonPath<'TDoc> tableName jsonPath sqlProps =
Custom.list<'TDoc>
(Query.byPathMatch (Query.find tableName)) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps
/// <summary>Retrieve documents matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given JSON Path expression</returns>
let ByJsonPath<'TDoc>(tableName, jsonPath, sqlProps) =
Custom.List<'TDoc>(
Query.byPathMatch (Query.find tableName),
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve documents matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
[<CompiledName "FSharpByJsonPathOrdered">]
let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps =
Custom.list<'TDoc>
(Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL)
[ "@path", Sql.string jsonPath ]
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve documents matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, sqlProps) =
Custom.List<'TDoc>(
Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL,
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields sqlProps =
Custom.single<'TDoc>
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1"
(addFieldParams fields [])
fromData<'TDoc>
sqlProps
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps =
Custom.single<'TDoc>
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1"
(addFieldParams queryFields [])
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
addFieldParams queryFields [],
fromData<'TDoc>,
sqlProps)
/// <summary>Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByContains">]
let firstByContains<'TDoc> tableName (criteria: obj) sqlProps =
Custom.single<'TDoc>
$"{Query.byContains (Query.find tableName)} LIMIT 1"
[ jsonParam "@criteria" criteria ]
fromData<'TDoc>
sqlProps
/// <summary>Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byContains (Query.find tableName)} LIMIT 1",
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByContainsOrdered">]
let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps =
Custom.single<'TDoc>
$"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1"
[ jsonParam "@criteria" criteria ]
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve the first document matching a JSON containment query (<tt>@&gt;</tt>) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, criteria: obj, orderFields, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
/// <summary>Retrieve the first document matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath sqlProps =
Custom.single<'TDoc>
$"{Query.byPathMatch (Query.find tableName)} LIMIT 1"
[ "@path", Sql.string jsonPath ]
fromData<'TDoc>
sqlProps
/// <summary>Retrieve the first document matching a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byPathMatch (Query.find tableName)} LIMIT 1",
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
[<CompiledName "FSharpFirstByJsonPathOrdered">]
let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps =
Custom.single<'TDoc>
$"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1"
[ "@path", Sql.string jsonPath ]
fromData<'TDoc>
sqlProps
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<tt>@?</tt>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, jsonPath, orderFields, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
/// <summary>Commands to update documents</summary>
[<RequireQualifiedAccess>]
module Update =
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) sqlProps =
Custom.nonQuery
(Query.byId (Query.update tableName) docId) [ idParam docId; jsonParam "@data" document ] sqlProps
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps =
byId tableName (idFunc document) document sqlProps
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) =
byFunc tableName idFunc.Invoke document sqlProps
/// <summary>Commands to patch (partially update) documents</summary>
[<RequireQualifiedAccess>]
module Patch =
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] sqlProps
/// <summary>
/// Patch documents using a JSON field comparison query in the <tt>WHERE</tt> clause (<tt>-&gt;&gt; =</tt>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.byFields (Query.patch tableName) howMatched fields)
(addFieldParams fields [ jsonParam "@data" patch ])
sqlProps
/// <summary>Patch documents using a JSON containment query in the <tt>WHERE</tt> clause (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.byContains (Query.patch tableName))
[ jsonParam "@data" patch; jsonParam "@criteria" criteria ]
sqlProps
/// <summary>Patch documents using a JSON Path match query in the <tt>WHERE</tt> clause (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.byPathMatch (Query.patch tableName))
[ jsonParam "@data" patch; "@path", Sql.string jsonPath ]
sqlProps
/// <summary>Commands to remove fields from documents</summary>
[<RequireQualifiedAccess>]
module RemoveFields =
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames sqlProps =
Custom.nonQuery
(Query.byId (Query.removeFields tableName) docId) [ idParam docId; fieldNameParams fieldNames ] sqlProps
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames sqlProps =
Custom.nonQuery
(Query.byFields (Query.removeFields tableName) howMatched fields)
(addFieldParams fields [ fieldNameParams fieldNames ])
sqlProps
/// <summary>Remove fields from documents via a JSON containment query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) fieldNames sqlProps =
Custom.nonQuery
(Query.byContains (Query.removeFields tableName))
[ jsonParam "@criteria" criteria; fieldNameParams fieldNames ]
sqlProps
/// <summary>Remove fields from documents via a JSON Path match query (<tt>@?</tt>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "FSharpByJsonPath">]
let byJsonPath tableName jsonPath fieldNames sqlProps =
Custom.nonQuery
(Query.byPathMatch (Query.removeFields tableName))
[ "@path", Sql.string jsonPath; fieldNameParams fieldNames ]
sqlProps
/// <summary>Commands to delete documents</summary>
[<RequireQualifiedAccess>]
module Delete =
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) sqlProps =
Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] sqlProps
/// <summary>Delete documents by matching a JSON field comparison query (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.nonQuery
(Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) sqlProps
/// <summary>Delete documents by matching a JSON contains query (<tt>@&gt;</tt>)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TCriteria) sqlProps =
Custom.nonQuery (Query.byContains (Query.delete tableName)) [ jsonParam "@criteria" criteria ] sqlProps
/// <summary>Delete documents by matching a JSON Path match query (@?)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="sqlProps">The <tt>SqlProps</tt> to use to execute the query</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath sqlProps =
Custom.nonQuery (Query.byPathMatch (Query.delete tableName)) [ "@path", Sql.string jsonPath ] sqlProps

View File

@@ -3,7 +3,6 @@
<PropertyGroup>
<Description>Use SQLite as a document database</Description>
<PackageTags>JSON Document SQLite</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -15,8 +14,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -71,7 +71,7 @@ module WithConn =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, conn) =
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
[<RequireQualifiedAccess>]
@@ -144,7 +144,7 @@ module Find =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
@@ -247,7 +247,7 @@ type SqliteConnectionCSharpCompatExtensions =
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) =
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)

View File

@@ -1,488 +1,270 @@
namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite
/// <summary>F# extensions for the SqliteConnection type</summary>
/// F# extensions for the SqliteConnection type
[<AutoOpen>]
module Extensions =
type SqliteConnection with
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
/// Execute a query that returns a list of results
member conn.customList<'TDoc> query parameters mapFunc =
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns><tt>Some</tt> with the first matching result, or <tt>None</tt> if not found</returns>
/// Execute a query that returns one or no results
member conn.customSingle<'TDoc> query parameters mapFunc =
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// Execute a query that does not return a value
member conn.customNonQuery query parameters =
WithConn.Custom.nonQuery query parameters conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
/// Execute a query that returns a scalar value
member conn.customScalar<'T when 'T: struct> query parameters mapFunc =
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// Create a document table
member conn.ensureTable name =
WithConn.Definition.ensureTable name conn
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// Create an index on a document table
member conn.ensureFieldIndex tableName indexName fields =
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) =
WithConn.Document.insert<'TDoc> tableName document conn
WithConn.insert<'TDoc> tableName document conn
/// <summary>
/// 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 the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
member conn.save<'TDoc> tableName (document: 'TDoc) =
WithConn.Document.save tableName document conn
WithConn.save tableName document conn
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
/// Count all documents in a table
member conn.countAll tableName =
WithConn.Count.all tableName conn
/// <summary>Count matching documents using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
/// Count matching documents using a comparison on JSON fields
member conn.countByFields tableName howMatched fields =
WithConn.Count.byFields tableName howMatched fields conn
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
/// Determine if a document exists for the given ID
member conn.existsById tableName (docId: 'TKey) =
WithConn.Exists.byId tableName docId conn
/// <summary>Determine if a document exists using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
/// Determine if a document exists using a comparison on JSON fields
member conn.existsByFields tableName howMatched fields =
WithConn.Exists.byFields tableName howMatched fields conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
/// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName =
WithConn.Find.all<'TDoc> tableName conn
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
/// Retrieve all documents in the given table ordered by the given fields in the document
member conn.findAllOrdered<'TDoc> tableName orderFields =
WithConn.Find.allOrdered<'TDoc> tableName orderFields conn
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns><tt>Some</tt> with the document if found, <tt>None</tt> otherwise</returns>
/// Retrieve a document by its ID
member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) =
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
/// Retrieve documents via a comparison on JSON fields
member conn.findByFields<'TDoc> tableName howMatched fields =
WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields
/// in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns><tt>Some</tt> with the first document, or <tt>None</tt> if not found</returns>
/// Retrieve documents via a comparison on JSON fields, returning only the first result
member conn.findFirstByFields<'TDoc> tableName howMatched fields =
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the
/// given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <tt>Some</tt> with the first document ordered by the given fields, or <tt>None</tt> if not found
/// </returns>
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning
/// only the first result
member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
WithConn.Update.byId tableName docId document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the
/// document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithConn.Update.byFunc tableName idFunc document conn
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch a document by its ID
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <tt>WHERE</tt> clause (<tt>-&gt;&gt; =</tt>,
/// etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch documents using a comparison on JSON fields
member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
WithConn.Patch.byFields tableName howMatched fields patch conn
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// Remove fields from a document by the document's ID
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// Remove a field from a document via a comparison on JSON fields in the document
member conn.removeFieldsByFields tableName howMatched fields fieldNames =
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// Delete a document by its ID
member conn.deleteById tableName (docId: 'TKey) =
WithConn.Delete.byId tableName docId conn
/// <summary>Delete documents by matching a JSON field comparison query (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// Delete documents by matching a comparison on JSON fields
member conn.deleteByFields tableName howMatched fields =
WithConn.Delete.byFields tableName howMatched fields conn
open System.Runtime.CompilerServices
/// <summary>C# extensions on the SqliteConnection type</summary>
/// C# extensions on the SqliteConnection type
type SqliteConnectionCSharpExtensions =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
/// Execute a query that returns a list of results
[<Extension>]
static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>The first matching result, or <tt>null</tt> if not found</returns>
/// Execute a query that returns one or no results
[<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>(
static member inline CustomSingle<'TDoc when 'TDoc: null>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns no results</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// Execute a query that does not return a value
[<Extension>]
static member inline CustomNonQuery(conn, query, parameters) =
WithConn.Custom.nonQuery query parameters conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
/// Execute a query that returns a scalar value
[<Extension>]
static member inline CustomScalar<'T when 'T: struct>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// <summary>Create a document table</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// Create a document table
[<Extension>]
static member inline EnsureTable(conn, name) =
WithConn.Definition.ensureTable name conn
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// Create an index on one or more fields in a document table
[<Extension>]
static member inline EnsureFieldIndex(conn, tableName, indexName, fields) =
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// <summary>Insert a new document</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// Insert a new document
[<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.Document.insert<'TDoc> tableName document conn
WithConn.insert<'TDoc> tableName document conn
/// <summary>Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.Document.save<'TDoc> tableName document conn
WithConn.save<'TDoc> tableName document conn
/// <summary>Count all documents in a table</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
/// Count all documents in a table
[<Extension>]
static member inline CountAll(conn, tableName) =
WithConn.Count.all tableName conn
/// <summary>Count matching documents using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
/// Count matching documents using a comparison on JSON fields
[<Extension>]
static member inline CountByFields(conn, tableName, howMatched, fields) =
WithConn.Count.byFields tableName howMatched fields conn
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
/// Determine if a document exists for the given ID
[<Extension>]
static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Exists.byId tableName docId conn
/// <summary>Determine if a document exists using JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
/// Determine if a document exists using a comparison on JSON fields
[<Extension>]
static member inline ExistsByFields(conn, tableName, howMatched, fields) =
WithConn.Exists.byFields tableName howMatched fields conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
/// Retrieve all documents in the given table
[<Extension>]
static member inline FindAll<'TDoc>(conn, tableName) =
WithConn.Find.All<'TDoc>(tableName, conn)
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
/// Retrieve all documents in the given table ordered by the given fields in the document
[<Extension>]
static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) =
WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn)
/// <summary>Retrieve a document by its ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The document if found, <tt>null</tt> otherwise</returns>
/// Retrieve a document by its ID
[<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) =
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) =
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// <summary>Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
/// Retrieve documents via a comparison on JSON fields
[<Extension>]
static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// <summary>
/// Retrieve documents matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
[<Extension>]
static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) =
WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first document, or <tt>null</tt> if not found</returns>
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, fields) =
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) =
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<tt>-&gt;&gt; =</tt>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <tt>null</tt> if not found</returns>
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only
/// the first result
[<Extension>]
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>(
conn, tableName, howMatched, queryFields, orderFields) =
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID
[<Extension>]
static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) =
WithConn.Update.byId tableName docId document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<Extension>]
static member inline UpdateByFunc<'TKey, 'TDoc>(
conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
WithConn.Update.ByFunc(tableName, idFunc, document, conn)
static member inline UpdateByFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) =
WithConn.Update.ByFunc(tableName, idFunc, doc, conn)
/// <summary>Patch a document by its ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch a document by its ID
[<Extension>]
static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <tt>WHERE</tt> clause (<tt>-&gt;&gt; =</tt>, etc.)
/// </summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch documents using a comparison on JSON fields
[<Extension>]
static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) =
WithConn.Patch.byFields tableName howMatched fields patch conn
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// Remove fields from a document by the document's ID
[<Extension>]
static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) =
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// Remove fields from documents via a comparison on JSON fields in the document
[<Extension>]
static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// <summary>Delete a document by its ID</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// Delete a document by its ID
[<Extension>]
static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Delete.byId tableName docId conn
/// <summary>Delete documents by matching a JSON field comparison query (<tt>-&gt;&gt; =</tt>, etc.)</summary>
/// <param name="conn">The <tt>SqliteConnection</tt> on which to run the query</param>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// Delete documents by matching a comparison on JSON fields
[<Extension>]
static member inline DeleteByFields(conn, tableName, howMatched, fields) =
WithConn.Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead; will be removed in v4">]
static member inline DeleteByField(conn, tableName, field) =
conn.DeleteByFields(tableName, Any, [ field ])

File diff suppressed because it is too large Load Diff

View File

@@ -5,16 +5,11 @@ This package provides a lightweight document library backed by [SQLite](https://
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Address documents via ID or via comparison on any field
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions
- Use building blocks for more complex queries
## Upgrading from v3
There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details.
## Getting Started
Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library:
@@ -77,28 +72,28 @@ Count customers in Atlanta:
```csharp
// C#; parameters are table name, field, operator, and value
// Count.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task<long>>
var customerCount = await Count.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Atlanta")]);
// Count.ByField type signature is Func<string, Field, Task<long>>
var customerCount = await Count.ByField("customer", Field.EQ("City", "Atlanta"));
```
```fsharp
// F#
// Count.byFields type signature is string -> FieldMatch -> Field seq -> Task<int64>
let! customerCount = Count.byFields "customer" Any [ Field.Equal "City" "Atlanta" ]
// Count.byField type signature is string -> Field -> Task<int64>
let! customerCount = Count.byField "customer" (Field.EQ "City" "Atlanta")
```
Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp
// C#; parameters are same as above, except return is void
// Delete.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task>
await Delete.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Chicago")]);
// Delete.ByField type signature is Func<string, Field, Task>
await Delete.ByField("customer", Field.EQ("City", "Chicago"));
```
```fsharp
// F#
// Delete.byFields type signature is string -> FieldMatch -> Field seq -> Task<unit>
do! Delete.byFields "customer" Any [ Field.Equal "City" "Chicago" ]
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit>
do! Delete.byField "customer" (Field.EQ "City" "Chicago")
```
## More Information

File diff suppressed because it is too large Load Diff

View File

@@ -205,7 +205,7 @@ public class PostgresCSharpExtensionTests
}
})
]),
TestList("Save",
TestList("save",
[
TestCase("succeeds when a document is inserted", async () =>
{
@@ -253,7 +253,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var theCount = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
[Field.EQ("Value", "purple")]);
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
TestCase("CountByContains succeeds", async () =>
@@ -303,7 +303,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Exists("Sub")]);
var exists = await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EX("Sub")]);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when documents do not exist", async () =>
@@ -313,7 +313,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var exists =
await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "six")]);
await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]);
Expect.isFalse(exists, "There should not have been existing documents");
})
]),
@@ -450,7 +450,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
[Field.EQ("Value", "another")]);
Expect.equal(docs.Count, 1, "There should have been one document returned");
}),
TestCase("succeeds when documents are not found", async () =>
@@ -460,7 +460,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
[Field.EQ("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
})
]),
@@ -473,7 +473,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id")]);
[Field.EQ("Value", "purple")], [Field.Named("Id")]);
Expect.hasLength(docs, 2, "There should have been two document returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"The documents were not ordered correctly");
@@ -485,7 +485,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id DESC")]);
[Field.EQ("Value", "purple")], [Field.Named("Id DESC")]);
Expect.hasLength(docs, 2, "There should have been two document returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"The documents were not ordered correctly");
@@ -599,7 +599,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
[Field.EQ("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
@@ -610,7 +610,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
[Field.EQ("Value", "purple")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned");
}),
@@ -621,7 +621,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
[Field.EQ("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned");
})
]),
@@ -634,7 +634,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id")]);
[Field.EQ("Value", "purple")], [Field.Named("Id")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("five", doc.Id, "An incorrect document was returned");
}),
@@ -645,7 +645,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id DESC")]);
[Field.EQ("Value", "purple")], [Field.Named("Id DESC")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("four", doc.Id, "An incorrect document was returned");
})
@@ -859,10 +859,10 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")],
new { NumValue = 77 });
var after = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("NumValue", 77)]);
[Field.EQ("NumValue", "77")]);
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
@@ -873,7 +873,7 @@ public class PostgresCSharpExtensionTests
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")],
new { Foo = "green" });
})
]),
@@ -975,7 +975,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Sub", "Value"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
@@ -988,7 +988,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
@@ -1002,7 +1002,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs();
// This not raising an exception is the test
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Nothing"]);
}),
TestCase("succeeds when no document is matched", async () =>
@@ -1012,7 +1012,7 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.NotEqual("Abracadabra", "apple")], ["Value"]);
[Field.NE("Abracadabra", "apple")], ["Value"]);
})
]),
TestList("RemoveFieldsByContains",
@@ -1134,7 +1134,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NotEqual("Value", "purple")]);
await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]);
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 2, "There should have been 2 documents remaining");
}),
@@ -1144,7 +1144,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "crimson")]);
await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]);
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})

View File

@@ -111,7 +111,7 @@ public static class PostgresCSharpTests
[
TestCase("succeeds when a parameter is added", () =>
{
var paramList = Parameters.AddFields([Field.Equal("it", "242")], []).ToList();
var paramList = Parameters.AddFields([Field.EQ("it", "242")], []).ToList();
Expect.hasLength(paramList, 1, "There should have been a parameter added");
var (name, value) = paramList[0];
Expect.equal(name, "@field0", "Field parameter name not correct");
@@ -119,7 +119,7 @@ public static class PostgresCSharpTests
}),
TestCase("succeeds when multiple independent parameters are added", () =>
{
var paramList = Parameters.AddFields([Field.Equal("me", "you"), Field.Greater("us", "them")],
var paramList = Parameters.AddFields([Field.EQ("me", "you"), Field.GT("us", "them")],
[Parameters.Id(14)]).ToList();
Expect.hasLength(paramList, 3, "There should have been 2 parameters added");
var (name, value) = paramList[0];
@@ -134,13 +134,13 @@ public static class PostgresCSharpTests
}),
TestCase("succeeds when a parameter is not added", () =>
{
var paramList = Parameters.AddFields([Field.Exists("tacos")], []).ToList();
var paramList = Parameters.AddFields([Field.EX("tacos")], []).ToList();
Expect.isEmpty(paramList, "There should not have been any parameters added");
}),
TestCase("succeeds when two parameters are added for one field", () =>
{
var paramList =
Parameters.AddFields([Field.Between("that", "eh", "zed").WithParameterName("@test")], []).ToList();
Parameters.AddFields([Field.BT("that", "eh", "zed").WithParameterName("@test")], []).ToList();
Expect.hasLength(paramList, 2, "There should have been 2 parameters added");
var (name, value) = paramList[0];
Expect.equal(name, "@testmin", "Minimum field name not correct");
@@ -170,11 +170,7 @@ public static class PostgresCSharpTests
Expect.isTrue(false, "The parameter was not a StringArray type");
}
})
]),
TestCase("None succeeds", () =>
{
Expect.isEmpty(Parameters.None, "The no-params sequence should be empty");
})
])
]);
/// <summary>
@@ -184,60 +180,54 @@ public static class PostgresCSharpTests
[
TestList("WhereByFields",
[
TestCase("succeeds for a single field when a logical comparison is passed", () =>
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.Any,
[Field.Greater("theField", "0").WithParameterName("@test")]),
[Field.GT("theField", "0").WithParameterName("@test")]),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when an existence comparison is passed", () =>
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NotExists("thatField")]),
Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between comparison is passed with numeric values", () =>
TestCase("succeeds for a single field when a between operator is passed with numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.All,
[Field.Between("aField", 50, 99).WithParameterName("@range")]),
[Field.BT("aField", 50, 99).WithParameterName("@range")]),
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between comparison is passed with non-numeric values", () =>
TestCase("succeeds for a single field when a between operator is passed with non-numeric values", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.Any,
[Field.Between("field0", "a", "b").WithParameterName("@alpha")]),
[Field.BT("field0", "a", "b").WithParameterName("@alpha")]),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical comparisons", () =>
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.All,
[Field.Equal("theFirst", "1"), Field.Equal("numberTwo", "2")]),
[Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence comparison", () =>
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.Any,
[Field.NotExists("thatField"), Field.GreaterOrEqual("thisField", 18)]),
[Field.NEX("thatField"), Field.GE("thisField", 18)]),
"data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0",
"WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between comparisons", () =>
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.All,
[Field.Between("aField", 50, 99), Field.Between("anotherField", "a", "b")]),
[Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]),
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
}),
TestCase("succeeds for a field with an InArray comparison", () =>
{
Expect.equal(
Postgres.Query.WhereByFields(FieldMatch.All, [Field.InArray("theField", "the_table", ["q", "r"])]),
"data->'theField' ?| @field0", "WHERE clause not correct");
})
]),
TestList("WhereById",
@@ -305,7 +295,7 @@ public static class PostgresCSharpTests
}),
TestCase("ByFields succeeds", () =>
{
Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.Greater("That", 14)]),
Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]),
"unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct");
}),
TestCase("ByContains succeeds", () =>
@@ -320,12 +310,30 @@ public static class PostgresCSharpTests
})
]);
/// <summary>
/// Tests which do not hit the database
/// </summary>
private static readonly Test Unit = TestList("Unit",
[
ParametersTests,
QueryTests
]);
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
];
/// <summary>
/// Add the test documents to the database
/// </summary>
internal static async Task LoadDocs()
{
foreach (var doc in JsonDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
/// <summary>
@@ -540,83 +548,6 @@ public static class PostgresCSharpTests
{
// This is what should have happened
}
}),
TestCase("succeeds when adding a numeric auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.Number);
Configuration.UseIdField("Key");
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "one" });
await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "two" });
await Document.Insert(PostgresDb.TableName, new NumIdDocument { Key = 77, Text = "three" });
await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "four" });
var after = await Find.AllOrdered<NumIdDocument>(PostgresDb.TableName, [Field.Named("n:Key")]);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78],
"The IDs were not generated correctly");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
Configuration.UseIdField("Id");
}
}),
TestCase("succeeds when adding a GUID auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.Guid);
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" });
var after = await Find.All<JsonDocument>(PostgresDb.TableName);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs");
Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
}
}),
TestCase("succeeds when adding a RandomString auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.RandomString);
Configuration.UseIdStringLength(44);
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" });
await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" });
var after = await Find.All<JsonDocument>(PostgresDb.TableName);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.equal(after.Count(x => x.Id.Length == 44), 3,
"Three of the IDs should have been 44-character random strings");
Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
Configuration.UseIdStringLength(16);
}
})
]),
TestList("Save",
@@ -673,7 +604,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Between("NumValue", 10, 20)]);
[Field.BT("NumValue", 10, 20)]);
Expect.equal(theCount, 3, "There should have been 3 matching documents");
}),
TestCase("succeeds for non-numeric range", async () =>
@@ -682,7 +613,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.All,
[Field.Between("Value", "aardvark", "apple")]);
[Field.BT("Value", "aardvark", "apple")]);
Expect.equal(theCount, 1, "There should have been 1 matching document");
})
]),
@@ -735,7 +666,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NotExists("Sub")]);
var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NEX("Sub")]);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when documents do not exist", async () =>
@@ -743,8 +674,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("NumValue", "six")]);
var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]);
Expect.isFalse(exists, "There should not have been existing documents");
})
]),
@@ -876,16 +806,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
Expect.hasLength(docs, 1, "There should have been one document returned");
}),
TestCase("succeeds when documents are found using IN with numeric field", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.All,
[Field.In("NumValue", [2, 4, 6, 8])]);
[Field.EQ("Value", "another")]);
Expect.hasLength(docs, 1, "There should have been one document returned");
}),
TestCase("succeeds when documents are not found", async () =>
@@ -894,27 +815,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
}),
TestCase("succeeds for InArray when matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await Definition.EnsureTable(PostgresDb.TableName);
foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(PostgresDb.TableName, doc);
var docs = await Find.ByFields<ArrayDocument>(PostgresDb.TableName, FieldMatch.All,
[Field.InArray("Values", PostgresDb.TableName, ["c"])]);
Expect.hasLength(docs, 2, "There should have been two document returned");
}),
TestCase("succeeds for InArray when no matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await Definition.EnsureTable(PostgresDb.TableName);
foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(PostgresDb.TableName, doc);
var docs = await Find.ByFields<ArrayDocument>(PostgresDb.TableName, FieldMatch.All,
[Field.InArray("Values", PostgresDb.TableName, ["j"])]);
[Field.EQ("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
})
]),
@@ -926,7 +827,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id")]);
[Field.EQ("Value", "purple")], [Field.Named("Id")]);
Expect.hasLength(docs, 2, "There should have been two document returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"The documents were not ordered correctly");
@@ -937,7 +838,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id DESC")]);
[Field.EQ("Value", "purple")], [Field.Named("Id DESC")]);
Expect.hasLength(docs, 2, "There should have been two document returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"The documents were not ordered correctly");
@@ -1042,7 +943,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
[Field.EQ("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
@@ -1052,7 +953,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
[Field.EQ("Value", "purple")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned");
}),
@@ -1062,7 +963,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
[Field.EQ("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned");
})
]),
@@ -1074,7 +975,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var doc = await Find.FirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id")]);
[Field.EQ("Value", "purple")], [Field.Named("Id")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("five", doc.Id, "An incorrect document was returned");
}),
@@ -1084,7 +985,7 @@ public static class PostgresCSharpTests
await LoadDocs();
var doc = await Find.FirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")], [Field.Named("Id DESC")]);
[Field.EQ("Value", "purple")], [Field.Named("Id DESC")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("four", doc.Id, "An incorrect document was returned");
})
@@ -1298,9 +1199,9 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")],
new { NumValue = 77 });
var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "77")]);
var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "77")]);
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
@@ -1311,7 +1212,7 @@ public static class PostgresCSharpTests
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")],
new { Foo = "green" });
})
]),
@@ -1413,7 +1314,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Sub", "Value"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
@@ -1425,7 +1326,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
@@ -1438,7 +1339,7 @@ public static class PostgresCSharpTests
await LoadDocs();
// This not raising an exception is the test
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")],
["Nothing"]);
}),
TestCase("succeeds when no document is matched", async () =>
@@ -1446,8 +1347,8 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
// This not raising an exception is the test
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.NotEqual("Abracadabra", "apple")], ["Value"]);
await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")],
["Value"]);
})
]),
TestList("ByContains",
@@ -1565,7 +1466,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")]);
await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]);
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
@@ -1574,7 +1475,7 @@ public static class PostgresCSharpTests
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "crimson")]);
await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]);
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})

View File

@@ -1,3 +1,4 @@
using BitBadger.Documents.Postgres;
using Npgsql;
using Npgsql.FSharp;
using ThrowawayDb.Postgres;

View File

@@ -221,8 +221,7 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var theCount = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
var theCount = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]);
Expect.equal(theCount, 2L, "There should have been 2 matching documents");
}),
TestList("ExistsById",
@@ -254,8 +253,7 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var exists = await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.GreaterOrEqual("NumValue", 10)]);
var exists = await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
@@ -265,7 +263,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var exists =
await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Nothing", "none")]);
await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]);
Expect.isFalse(exists, "There should not have been any existing documents");
})
]),
@@ -359,7 +357,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)]);
[Field.GT("NumValue", 15)]);
Expect.equal(docs.Count, 2, "There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
@@ -369,7 +367,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
[Field.EQ("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
})
]),
@@ -382,7 +380,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id")]);
[Field.GT("NumValue", 15)], [Field.Named("Id")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"There should have been two documents returned");
}),
@@ -393,7 +391,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id DESC")]);
[Field.GT("NumValue", 15)], [Field.Named("Id DESC")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"There should have been two documents returned");
})
@@ -407,7 +405,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
[Field.EQ("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
@@ -418,7 +416,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")]);
[Field.EQ("Sub.Foo", "green")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned");
}),
@@ -429,7 +427,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
[Field.EQ("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned");
})
]),
@@ -442,7 +440,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")], [Field.Named("Sub.Bar")]);
[Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("two", doc!.Id, "An incorrect document was returned");
}),
@@ -453,7 +451,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]);
[Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("four", doc!.Id, "An incorrect document was returned");
})
@@ -549,9 +547,9 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")],
new { NumValue = 77 });
var after = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 77)]);
var after = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]);
Expect.equal(after, 2L, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
@@ -562,7 +560,7 @@ public static class SqliteCSharpExtensionTests
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")],
new { Foo = "green" });
})
]),
@@ -606,7 +604,7 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)],
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)],
["Sub"]);
var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
@@ -619,7 +617,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs();
// This not raising an exception is the test
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)],
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)],
["Nothing"]);
}),
TestCase("succeeds when no document is matched", async () =>
@@ -628,8 +626,8 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
// This not raising an exception is the test
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.NotEqual("Abracadabra", "apple")], ["Value"]);
await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")],
["Value"]);
})
]),
TestList("DeleteById",
@@ -663,7 +661,7 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NotEqual("Value", "purple")]);
await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]);
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 2L, "There should have been 2 documents remaining");
}),
@@ -673,7 +671,7 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "crimson")]);
await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]);
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})

View File

@@ -22,60 +22,45 @@ public static class SqliteCSharpTests
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.Any,
[Field.Greater("theField", 0).WithParameterName("@test")]),
Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.GT("theField", 0).WithParameterName("@test")]),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NotExists("thatField")]),
Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.Between("aField", 50, 99).WithParameterName("@range")]),
[Field.BT("aField", 50, 99).WithParameterName("@range")]),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.Equal("theFirst", "1"), Field.Equal("numberTwo", "2")]),
Sqlite.Query.WhereByFields(FieldMatch.All, [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.Any,
[Field.NotExists("thatField"), Field.GreaterOrEqual("thisField", 18)]),
Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.Between("aField", 50, 99), Field.Between("anotherField", "a", "b")]),
[Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]),
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
}),
TestCase("succeeds for a field with an In comparison", () =>
{
Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.All, [Field.In("this", ["a", "b", "c"])]),
"data->>'this' IN (@field0_0, @field0_1, @field0_2)", "WHERE clause not correct");
}),
TestCase("succeeds for a field with an InArray comparison", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All, [Field.InArray("this", "the_table", ["a", "b"])]),
"EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (@field0_0, @field0_1))",
"WHERE clause not correct");
})
]),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Sqlite.Query.WhereById("abc"), "data->>'Id' = @id", "WHERE clause not correct");
Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
}),
TestCase("Patch succeeds", () =>
{
@@ -94,7 +79,7 @@ public static class SqliteCSharpTests
}),
TestCase("ByFields succeeds", () =>
{
Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.Greater("That", 14)]),
Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]),
"unit WHERE data->>'That' > @field0", "By-Field query not correct");
}),
TestCase("Definition.EnsureTable succeeds", () =>
@@ -124,7 +109,7 @@ public static class SqliteCSharpTests
#pragma warning disable CS0618
TestCase("AddField succeeds when adding a parameter", () =>
{
var paramList = Parameters.AddField("@field", Field.Equal("it", 99), []).ToList();
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList();
Expect.hasLength(paramList, 1, "There should have been a parameter added");
var theParam = paramList[0];
Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect");
@@ -132,7 +117,7 @@ public static class SqliteCSharpTests
}),
TestCase("AddField succeeds when not adding a parameter", () =>
{
var paramSeq = Parameters.AddField("@it", Field.Exists("Coffee"), []);
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []);
Expect.isEmpty(paramSeq, "There should not have been any parameters added");
}),
#pragma warning restore CS0618
@@ -144,12 +129,24 @@ public static class SqliteCSharpTests
// Results are exhaustively executed in the context of other tests
/// <summary>
/// Documents used for integration tests
/// </summary>
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
];
/// <summary>
/// Add the test documents to the database
/// </summary>
internal static async Task LoadDocs()
{
foreach (var doc in JsonDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
/// <summary>
@@ -328,86 +325,9 @@ public static class SqliteCSharpTests
{
// This is what is supposed to happen
}
}),
TestCase("succeeds when adding a numeric auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.Number);
Configuration.UseIdField("Key");
await using var db = await SqliteDb.BuildDb();
var before = await Count.All(SqliteDb.TableName);
Expect.equal(before, 0L, "There should be no documents in the table");
await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "one" });
await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "two" });
await Document.Insert(SqliteDb.TableName, new NumIdDocument { Key = 77, Text = "three" });
await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "four" });
var after = await Find.AllOrdered<NumIdDocument>(SqliteDb.TableName, [Field.Named("Key")]);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78],
"The IDs were not generated correctly");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
Configuration.UseIdField("Id");
}
}),
TestCase("succeeds when adding a GUID auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.Guid);
await using var db = await SqliteDb.BuildDb();
var before = await Count.All(SqliteDb.TableName);
Expect.equal(before, 0L, "There should be no documents in the table");
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" });
var after = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs");
Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
}
}),
TestCase("succeeds when adding a RandomString auto ID", async () =>
{
try
{
Configuration.UseAutoIdStrategy(AutoId.RandomString);
Configuration.UseIdStringLength(44);
await using var db = await SqliteDb.BuildDb();
var before = await Count.All(SqliteDb.TableName);
Expect.equal(before, 0L, "There should be no documents in the table");
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" });
var after = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.hasLength(after, 4, "There should have been 4 documents returned");
Expect.equal(after.Count(x => x.Id.Length == 44), 3,
"Three of the IDs should have been 44-character random strings");
Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
Configuration.UseIdStringLength(16);
}
})
]),
TestList("Save",
TestList("Document.Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
@@ -462,8 +382,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.Between("NumValue", 10, 20)]);
var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.BT("NumValue", 10, 20)]);
Expect.equal(theCount, 3L, "There should have been 3 matching documents");
}),
TestCase("succeeds for non-numeric range", async () =>
@@ -472,7 +391,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.Between("Value", "aardvark", "apple")]);
[Field.BT("Value", "aardvark", "apple")]);
Expect.equal(theCount, 1L, "There should have been 1 matching document");
})
])
@@ -509,8 +428,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.GreaterOrEqual("NumValue", 10)]);
var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
@@ -518,8 +436,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Nothing", "none")]);
var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]);
Expect.isFalse(exists, "There should not have been any existing documents");
})
])
@@ -611,45 +528,16 @@ public static class SqliteCSharpTests
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)]);
[Field.GT("NumValue", 15)]);
Expect.equal(docs.Count, 2, "There should have been two documents returned");
}),
TestCase("succeeds when documents are found using IN with numeric field", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.All,
[Field.In("NumValue", [2, 4, 6, 8])]);
Expect.hasLength(docs, 1, "There should have been one document returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
}),
TestCase("succeeds for InArray when matching documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await Definition.EnsureTable(SqliteDb.TableName);
foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
var docs = await Find.ByFields<ArrayDocument>(SqliteDb.TableName, FieldMatch.All,
[Field.InArray("Values", SqliteDb.TableName, ["c"])]);
Expect.hasLength(docs, 2, "There should have been two document returned");
}),
TestCase("succeeds for InArray when no matching documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await Definition.EnsureTable(SqliteDb.TableName);
foreach (var doc in ArrayDocument.TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
var docs = await Find.ByFields<ArrayDocument>(SqliteDb.TableName, FieldMatch.All,
[Field.InArray("Values", SqliteDb.TableName, ["j"])]);
[Field.EQ("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned");
})
]),
@@ -661,10 +549,9 @@ public static class SqliteCSharpTests
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
[Field.GT("NumValue", 15)], [Field.Named("Id")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"The documents were not sorted correctly");
"There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
@@ -672,32 +559,9 @@ public static class SqliteCSharpTests
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id DESC")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
[Field.GT("NumValue", 15)], [Field.Named("Id DESC")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"The documents were not sorted correctly");
}),
TestCase("succeeds when sorting case-sensitively", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.LessOrEqual("NumValue", 10)], [Field.Named("Value")]);
Expect.hasLength(docs, 3, "There should have been three documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "three|one|two",
"The documents were not sorted correctly");
}),
TestCase("succeeds when sorting case-insensitively", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.LessOrEqual("NumValue", 10)], [Field.Named("i:Value")]);
Expect.hasLength(docs, 3, "There should have been three documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "three|two|one",
"The documents were not sorted correctly");
"There should have been two documents returned");
})
]),
TestList("FirstByFields",
@@ -708,7 +572,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
[Field.EQ("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
@@ -718,7 +582,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")]);
[Field.EQ("Sub.Foo", "green")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned");
}),
@@ -728,7 +592,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var doc = await Find.FirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
[Field.EQ("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned");
})
]),
@@ -740,7 +604,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var doc = await Find.FirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")], [Field.Named("Sub.Bar")]);
[Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("two", doc!.Id, "An incorrect document was returned");
}),
@@ -750,7 +614,7 @@ public static class SqliteCSharpTests
await LoadDocs();
var doc = await Find.FirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]);
[Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("four", doc!.Id, "An incorrect document was returned");
})
@@ -856,9 +720,9 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")],
new { NumValue = 77 });
var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 77)]);
var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]);
Expect.equal(after, 2L, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
@@ -869,7 +733,7 @@ public static class SqliteCSharpTests
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")],
new { Foo = "green" });
})
])
@@ -916,7 +780,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)], ["Sub"]);
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], ["Sub"]);
var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.isNull(updated.Sub, "The sub-document should have been removed");
@@ -927,7 +791,7 @@ public static class SqliteCSharpTests
await LoadDocs();
// This not raising an exception is the test
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)],
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)],
["Nothing"]);
}),
TestCase("succeeds when no document is matched", async () =>
@@ -935,8 +799,8 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
// This not raising an exception is the test
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.NotEqual("Abracadabra", "apple")], ["Value"]);
await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")],
["Value"]);
})
])
]);
@@ -974,7 +838,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NotEqual("Value", "purple")]);
await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]);
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 2L, "There should have been 2 documents remaining");
}),
@@ -983,7 +847,7 @@ public static class SqliteCSharpTests
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.Equal("Value", "crimson")]);
await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.EQ("Value", "crimson")]);
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})

View File

@@ -1,11 +1,5 @@
namespace BitBadger.Documents.Tests.CSharp;
public class NumIdDocument
{
public int Key { get; set; } = 0;
public string Text { get; set; } = "";
}
public class SubDocument
{
public string Foo { get; set; } = "";
@@ -18,32 +12,4 @@ public class JsonDocument
public string Value { get; set; } = "";
public int NumValue { get; set; } = 0;
public SubDocument? Sub { get; set; } = null;
/// <summary>
/// A set of documents used for integration tests
/// </summary>
public static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
];
}
public class ArrayDocument
{
public string Id { get; set; } = "";
public string[] Values { get; set; } = [];
/// <summary>
/// A set of documents used for integration tests
/// </summary>
public static readonly List<ArrayDocument> TestDocuments =
[
new() { Id = "first", Values = ["a", "b", "c"] },
new() { Id = "second", Values = ["c", "d", "e"] },
new() { Id = "third", Values = ["x", "y", "z"] }
];
}

View File

@@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="CommonTests.fs" />
<Compile Include="Types.fs" />
<Compile Include="PostgresTests.fs" />
<Compile Include="PostgresExtensionTests.fs" />
<Compile Include="SqliteTests.fs" />
@@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,491 +6,320 @@ open Expecto
/// Test table name
let tbl = "test_table"
/// Unit tests for the Op DU
let comparisonTests = testList "Comparison.OpSql" [
test "Equal succeeds" {
Expect.equal (Equal "").OpSql "=" "The Equals SQL was not correct"
}
test "Greater succeeds" {
Expect.equal (Greater "").OpSql ">" "The Greater SQL was not correct"
}
test "GreaterOrEqual succeeds" {
Expect.equal (GreaterOrEqual "").OpSql ">=" "The GreaterOrEqual SQL was not correct"
}
test "Less succeeds" {
Expect.equal (Less "").OpSql "<" "The Less SQL was not correct"
}
test "LessOrEqual succeeds" {
Expect.equal (LessOrEqual "").OpSql "<=" "The LessOrEqual SQL was not correct"
}
test "NotEqual succeeds" {
Expect.equal (NotEqual "").OpSql "<>" "The NotEqual SQL was not correct"
}
test "Between succeeds" {
Expect.equal (Between("", "")).OpSql "BETWEEN" "The Between SQL was not correct"
}
test "In succeeds" {
Expect.equal (In []).OpSql "IN" "The In SQL was not correct"
}
test "InArray succeeds" {
Expect.equal (InArray("", [])).OpSql "?|" "The InArray SQL was not correct"
}
test "Exists succeeds" {
Expect.equal Exists.OpSql "IS NOT NULL" "The Exists SQL was not correct"
}
test "NotExists succeeds" {
Expect.equal NotExists.OpSql "IS NULL" "The NotExists SQL was not correct"
}
]
/// Unit tests for the Field class
let fieldTests = testList "Field" [
test "Equal succeeds" {
let field = Field.Equal "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Comparison (Equal 14) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "Greater succeeds" {
let field = Field.Greater "Great" "night"
Expect.equal field.Name "Great" "Field name incorrect"
Expect.equal field.Comparison (Greater "night") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "GreaterOrEqual succeeds" {
let field = Field.GreaterOrEqual "Nice" 88L
Expect.equal field.Name "Nice" "Field name incorrect"
Expect.equal field.Comparison (GreaterOrEqual 88L) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "Less succeeds" {
let field = Field.Less "Lesser" "seven"
Expect.equal field.Name "Lesser" "Field name incorrect"
Expect.equal field.Comparison (Less "seven") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "LessOrEqual succeeds" {
let field = Field.LessOrEqual "Nobody" "KNOWS";
Expect.equal field.Name "Nobody" "Field name incorrect"
Expect.equal field.Comparison (LessOrEqual "KNOWS") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NotEqual succeeds" {
let field = Field.NotEqual "Park" "here"
Expect.equal field.Name "Park" "Field name incorrect"
Expect.equal field.Comparison (NotEqual "here") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "Between succeeds" {
let field = Field.Between "Age" 18 49
Expect.equal field.Name "Age" "Field name incorrect"
Expect.equal field.Comparison (Between(18, 49)) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "In succeeds" {
let field = Field.In "Here" [| 8; 16; 32 |]
Expect.equal field.Name "Here" "Field name incorrect"
match field.Comparison with
| In values -> Expect.equal (List.ofSeq values) [ box 8; box 16; box 32 ] "Comparison incorrect"
| it -> Expect.isTrue false $"Expected In, received %A{it}"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "InArray succeeds" {
let field = Field.InArray "ArrayField" "table" [| "z" |]
Expect.equal field.Name "ArrayField" "Field name incorrect"
match field.Comparison with
| InArray (table, values) ->
Expect.equal table "table" "Comparison table incorrect"
Expect.equal (List.ofSeq values) [ box "z" ] "Comparison values incorrect"
| it -> Expect.isTrue false $"Expected InArray, received %A{it}"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "Exists succeeds" {
let field = Field.Exists "Groovy"
Expect.equal field.Name "Groovy" "Field name incorrect"
Expect.equal field.Comparison Exists "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NotExists succeeds" {
let field = Field.NotExists "Rad"
Expect.equal field.Name "Rad" "Field name incorrect"
Expect.equal field.Comparison NotExists "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
testList "NameToPath" [
test "succeeds for PostgreSQL and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL AsSql) "Path not constructed correctly"
}
test "succeeds for SQLite and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" SQLite AsSql) "Path not constructed correctly"
}
test "succeeds for PostgreSQL and a nested name" {
Expect.equal
"data#>>'{A,Long,Path,to,the,Property}'"
(Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL AsSql)
"Path not constructed correctly"
}
test "succeeds for SQLite and a nested name" {
Expect.equal
"data->'A'->'Long'->'Path'->'to'->'the'->>'Property'"
(Field.NameToPath "A.Long.Path.to.the.Property" SQLite AsSql)
"Path not constructed correctly"
}
]
test "WithParameterName succeeds" {
let field = (Field.Equal "Bob" "Tom").WithParameterName "@name"
Expect.isSome field.ParameterName "The parameter name should have been filled"
Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect"
}
test "WithQualifier succeeds" {
let field = (Field.Equal "Bill" "Matt").WithQualifier "joe"
Expect.isSome field.Qualifier "The table qualifier should have been filled"
Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
}
testList "Path" [
test "succeeds for a PostgreSQL single field with no qualifier" {
let field = Field.GreaterOrEqual "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL single field with a qualifier" {
let field = { Field.Less "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with no qualifier" {
let field = Field.Equal "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with a qualifier" {
let field = { Field.Equal "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a SQLite single field with no qualifier" {
let field = Field.GreaterOrEqual "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite single field with a qualifier" {
let field = { Field.Less "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with no qualifier" {
let field = Field.Equal "My.Nested.Field" "howdy"
Expect.equal "data->'My'->'Nested'->>'Field'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with a qualifier" {
let field = { Field.Equal "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->'Nest'->>'Away'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
]
]
/// Unit tests for the FieldMatch DU
let fieldMatchTests = testList "FieldMatch.ToString" [
test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
/// Unit tests for the ParameterName class
let parameterNameTests = testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
/// Unit tests for the AutoId DU
let autoIdTests = testList "AutoId" [
test "GenerateGuid succeeds" {
let autoId = AutoId.GenerateGuid()
Expect.isNotNull autoId "The GUID auto-ID should not have been null"
Expect.stringHasLength autoId 32 "The GUID auto-ID should have been 32 characters long"
Expect.equal autoId (autoId.ToLowerInvariant ()) "The GUID auto-ID should have been lowercase"
}
test "GenerateRandomString succeeds" {
[ 6; 8; 12; 20; 32; 57; 64 ]
|> List.iter (fun length ->
let autoId = AutoId.GenerateRandomString length
Expect.isNotNull autoId $"Random string ({length}) should not have been null"
Expect.stringHasLength autoId length $"Random string should have been {length} characters long"
Expect.equal autoId (autoId.ToLowerInvariant ()) $"Random string ({length}) should have been lowercase")
}
testList "NeedsAutoId" [
test "succeeds when no auto ID is configured" {
Expect.isFalse (AutoId.NeedsAutoId Disabled (obj ()) "id") "Disabled auto-ID never needs an automatic ID"
}
test "fails for any when the ID property is not found" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Number {| Key = "" |} "Id" |> ignore)
"Non-existent ID property should have thrown an exception"
}
test "succeeds for byte when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int8 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for byte when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int8 4 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for short when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int16 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for short when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int16 7 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for int when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for int when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 32 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for long when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0L |} "Id") "Zero ID should have returned true"
}
test "succeeds for long when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 80L |} "Id") "Non-zero ID should have returned false"
}
test "fails for number when the ID is not a number" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Number {| Id = "" |} "Id" |> ignore)
"Numeric ID against a string should have thrown an exception"
}
test "succeeds for GUID when the ID is blank" {
Expect.isTrue (AutoId.NeedsAutoId Guid {| Id = "" |} "Id") "Blank ID should have returned true"
}
test "succeeds for GUID when the ID is filled" {
Expect.isFalse (AutoId.NeedsAutoId Guid {| Id = "abc" |} "Id") "Filled ID should have returned false"
}
test "fails for GUID when the ID is not a string" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Guid {| Id = 8 |} "Id" |> ignore)
"String ID against a number should have thrown an exception"
}
test "succeeds for RandomString when the ID is blank" {
Expect.isTrue (AutoId.NeedsAutoId RandomString {| Id = "" |} "Id") "Blank ID should have returned true"
}
test "succeeds for RandomString when the ID is filled" {
Expect.isFalse (AutoId.NeedsAutoId RandomString {| Id = "x" |} "Id") "Filled ID should have returned false"
}
test "fails for RandomString when the ID is not a string" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId RandomString {| Id = 33 |} "Id" |> ignore)
"String ID against a number should have thrown an exception"
}
]
]
/// Unit tests for the Configuration module
let configurationTests = testList "Configuration" [
test "useSerializer succeeds" {
try
Configuration.useSerializer
{ new IDocumentSerializer with
member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}"""
member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T>
}
let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |}
Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used"
let deserialized = Configuration.serializer().Deserialize<obj> """{"Something":"here"}"""
Expect.isNull deserialized "Specified serializer should have returned null"
finally
Configuration.useSerializer DocumentSerializer.``default``
}
test "serializer returns configured serializer" {
Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ()))
"Serializer should have been the same"
}
test "useIdField / idField succeeds" {
try
Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect"
Configuration.useIdField "id"
Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field"
finally
Configuration.useIdField "Id"
}
test "useAutoIdStrategy / autoIdStrategy succeeds" {
try
Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect"
Configuration.useAutoIdStrategy Guid
Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly"
finally
Configuration.useAutoIdStrategy Disabled
}
test "useIdStringLength / idStringLength succeeds" {
try
Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect"
Configuration.useIdStringLength 33
Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly"
finally
Configuration.useIdStringLength 16
}
]
/// Unit tests for the Query module
let queryTests = testList "Query" [
test "statementWhere succeeds" {
Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly"
}
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
(Query.Definition.ensureTableFor "my.table" "JSONB")
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table" PostgreSQL)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table" SQLite)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
testList "ensureIndexOn" [
test "succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn
"test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL)
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
"((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ]
|> String.concat "")
"CREATE INDEX for multiple field statement incorrect"
}
test "succeeds for nested PostgreSQL field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))"
"CREATE INDEX for nested PostgreSQL field incorrect"
}
test "succeeds for nested SQLite field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->'a'->'b'->>'c'))"
"CREATE INDEX for nested SQLite field incorrect"
}
]
]
test "insert succeeds" {
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct"
}
test "save succeeds" {
Expect.equal
(Query.save tbl)
$"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
test "count succeeds" {
Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "exists succeeds" {
Expect.equal
(Query.exists tbl "turkey")
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it"
"Exists query not correct"
}
test "find succeeds" {
Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct"
}
test "update succeeds" {
Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct"
}
test "delete succeeds" {
Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct"
}
testList "orderBy" [
test "succeeds for no fields" {
Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)"
Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)"
}
test "succeeds for PostgreSQL with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] PostgreSQL)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for SQLite with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] SQLite)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
PostgreSQL)
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for SQLite with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
SQLite)
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] PostgreSQL)
" ORDER BY (data->>'Test')::numeric"
"Order By not constructed correctly for numeric field"
}
test "succeeds for SQLite numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] SQLite)
" ORDER BY data->>'Test'"
"Order By not constructed correctly for numeric field"
}
test "succeeds for PostgreSQL case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field DESC NULLS FIRST" ] PostgreSQL)
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST"
"Order By not constructed correctly for case-insensitive field"
}
test "succeeds for SQLite case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field ASC NULLS LAST" ] SQLite)
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST"
"Order By not constructed correctly for case-insensitive field"
}
]
]
/// Tests which do not hit the database
let all = testList "Common" [
comparisonTests
fieldTests
fieldMatchTests
parameterNameTests
autoIdTests
queryTests
testSequenced configurationTests
]
let all =
testList "Common" [
testList "Op" [
test "EQ succeeds" {
Expect.equal (string EQ) "=" "The equals operator was not correct"
}
test "GT succeeds" {
Expect.equal (string GT) ">" "The greater than operator was not correct"
}
test "GE succeeds" {
Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct"
}
test "LT succeeds" {
Expect.equal (string LT) "<" "The less than operator was not correct"
}
test "LE succeeds" {
Expect.equal (string LE) "<=" "The less than or equal to operator was not correct"
}
test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct"
}
test "BT succeeds" {
Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct"""
}
test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct"""
}
test "NEX succeeds" {
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct"""
}
]
testList "Field" [
test "EQ succeeds" {
let field = Field.EQ "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Op EQ "Operator incorrect"
Expect.equal field.Value 14 "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "GT succeeds" {
let field = Field.GT "Great" "night"
Expect.equal field.Name "Great" "Field name incorrect"
Expect.equal field.Op GT "Operator incorrect"
Expect.equal field.Value "night" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "GE succeeds" {
let field = Field.GE "Nice" 88L
Expect.equal field.Name "Nice" "Field name incorrect"
Expect.equal field.Op GE "Operator incorrect"
Expect.equal field.Value 88L "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "LT succeeds" {
let field = Field.LT "Lesser" "seven"
Expect.equal field.Name "Lesser" "Field name incorrect"
Expect.equal field.Op LT "Operator incorrect"
Expect.equal field.Value "seven" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "LE succeeds" {
let field = Field.LE "Nobody" "KNOWS";
Expect.equal field.Name "Nobody" "Field name incorrect"
Expect.equal field.Op LE "Operator incorrect"
Expect.equal field.Value "KNOWS" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NE succeeds" {
let field = Field.NE "Park" "here"
Expect.equal field.Name "Park" "Field name incorrect"
Expect.equal field.Op NE "Operator incorrect"
Expect.equal field.Value "here" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "BT succeeds" {
let field = Field.BT "Age" 18 49
Expect.equal field.Name "Age" "Field name incorrect"
Expect.equal field.Op BT "Operator incorrect"
Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "EX succeeds" {
let field = Field.EX "Groovy"
Expect.equal field.Name "Groovy" "Field name incorrect"
Expect.equal field.Op EX "Operator incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NEX succeeds" {
let field = Field.NEX "Rad"
Expect.equal field.Name "Rad" "Field name incorrect"
Expect.equal field.Op NEX "Operator incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
testList "NameToPath" [
test "succeeds for PostgreSQL and a simple name" {
Expect.equal
"data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly"
}
test "succeeds for SQLite and a simple name" {
Expect.equal
"data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly"
}
test "succeeds for PostgreSQL and a nested name" {
Expect.equal
"data#>>'{A,Long,Path,to,the,Property}'"
(Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL)
"Path not constructed correctly"
}
test "succeeds for SQLite and a nested name" {
Expect.equal
"data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'"
(Field.NameToPath "A.Long.Path.to.the.Property" SQLite)
"Path not constructed correctly"
}
]
test "WithParameterName succeeds" {
let field = (Field.EQ "Bob" "Tom").WithParameterName "@name"
Expect.isSome field.ParameterName "The parameter name should have been filled"
Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect"
}
test "WithQualifier succeeds" {
let field = (Field.EQ "Bill" "Matt").WithQualifier "joe"
Expect.isSome field.Qualifier "The table qualifier should have been filled"
Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
}
testList "Path" [
test "succeeds for a PostgreSQL single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal
"this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a SQLite single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect"
}
]
]
testList "FieldMatch.ToString" [
test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
testList "Query" [
test "statementWhere succeeds" {
Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly"
}
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
(Query.Definition.ensureTableFor "my.table" "JSONB")
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table" PostgreSQL)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table" SQLite)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
testList "ensureIndexOn" [
test "succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn
"test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL)
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
"((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ]
|> String.concat "")
"CREATE INDEX for multiple field statement incorrect"
}
test "succeeds for nested PostgreSQL field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))"
"CREATE INDEX for nested PostgreSQL field incorrect"
}
test "succeeds for nested SQLite field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))"
"CREATE INDEX for nested SQLite field incorrect"
}
]
]
test "insert succeeds" {
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct"
}
test "save succeeds" {
Expect.equal
(Query.save tbl)
$"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
test "count succeeds" {
Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "exists succeeds" {
Expect.equal
(Query.exists tbl "turkey")
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it"
"Exists query not correct"
}
test "find succeeds" {
Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct"
}
test "update succeeds" {
Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct"
}
test "delete succeeds" {
Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct"
}
testList "orderBy" [
test "succeeds for no fields" {
Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)"
Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)"
}
test "succeeds for PostgreSQL with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] PostgreSQL)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for SQLite with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] SQLite)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
PostgreSQL)
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for SQLite with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
SQLite)
" ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] PostgreSQL)
" ORDER BY (data->>'Test')::numeric"
"Order By not constructed correctly for numeric field"
}
test "succeeds for SQLite numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] SQLite)
" ORDER BY data->>'Test'"
"Order By not constructed correctly for numeric field"
}
]
]
]

View File

@@ -214,7 +214,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! theCount = conn.countByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
let! theCount = conn.countByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ]
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "countByContains succeeds" {
@@ -257,7 +257,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.Exists "Sub" ]
let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EX "Sub" ]
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
@@ -265,7 +265,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "six" ]
let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "six" ]
Expect.isFalse exists "There should not have been existing documents"
}
]
@@ -390,7 +390,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "another" ]
let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "another" ]
Expect.equal (List.length docs) 1 "There should have been one document returned"
}
testTask "succeeds when documents are not found" {
@@ -398,7 +398,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "mauve" ]
let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "mauve" ]
Expect.isEmpty docs "There should have been no documents returned"
}
]
@@ -410,7 +410,7 @@ let integrationTests =
let! docs =
conn.findByFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "Value" "purple" ] [ Field.Named "Id" ]
PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly"
@@ -422,7 +422,7 @@ let integrationTests =
let! docs =
conn.findByFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "Value" "purple" ] [ Field.Named "Id DESC" ]
PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly"
@@ -524,8 +524,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "another" ]
let! doc = conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "another" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
@@ -534,8 +533,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
let! doc = conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "purple" ]
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned"
}
@@ -544,8 +542,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "absent" ]
let! doc = conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "absent" ]
Expect.isNone doc "There should not have been a document returned"
}
]
@@ -557,7 +554,7 @@ let integrationTests =
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] [ Field.Named "Id" ]
PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "five" doc.Value.Id "An incorrect document was returned"
}
@@ -568,7 +565,7 @@ let integrationTests =
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] [ Field.Named "Id DESC" ]
PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "four" doc.Value.Id "An incorrect document was returned"
}
@@ -753,8 +750,8 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
do! conn.patchByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "77" ]
do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "77" ]
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
@@ -764,7 +761,7 @@ let integrationTests =
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByFields PostgresDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |}
}
]
testList "patchByContains" [
@@ -814,9 +811,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -825,9 +822,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -852,11 +849,10 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
do! conn.removeFieldsByFields
PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -864,10 +860,10 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -876,15 +872,14 @@ let integrationTests =
do! loadDocs conn
// This not raising an exception is the test
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Nothing" ]
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ]
}
testTask "succeeds when no document is matched" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
// This not raising an exception is the test
do! conn.removeFieldsByFields
PostgresDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ]
}
]
testList "removeFieldsByContains" [
@@ -894,9 +889,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -905,9 +900,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -933,9 +928,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -944,9 +939,9 @@ let integrationTests =
do! loadDocs conn
do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -991,7 +986,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ]
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
@@ -1000,7 +995,7 @@ let integrationTests =
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByFields PostgresDb.TableName Any [ Field.Equal "Value" "crimson" ]
do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ]
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}

View File

@@ -83,14 +83,14 @@ let parametersTests = testList "Parameters" [
}
testList "addFieldParams" [
test "succeeds when a parameter is added" {
let paramList = addFieldParams [ Field.Where "it" (Equal "242") ] []
let paramList = addFieldParams [ Field.EQ "it" "242" ] []
Expect.hasLength paramList 1 "There should have been a parameter added"
let name, value = Seq.head paramList
Expect.equal name "@field0" "Field parameter name not correct"
Expect.equal value (Sql.string "242") "Parameter value not correct"
}
test "succeeds when multiple independent parameters are added" {
let paramList = addFieldParams [ Field.Equal "me" "you"; Field.Greater "us" "them" ] [ idParam 14 ]
let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ]
Expect.hasLength paramList 3 "There should have been 2 parameters added"
let p = Array.ofSeq paramList
Expect.equal (fst p[0]) "@id" "First field parameter name not correct"
@@ -101,11 +101,11 @@ let parametersTests = testList "Parameters" [
Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct"
}
test "succeeds when a parameter is not added" {
let paramList = addFieldParams [ Field.Exists "tacos" ] []
let paramList = addFieldParams [ Field.EX "tacos" ] []
Expect.isEmpty paramList "There should not have been any parameters added"
}
test "succeeds when two parameters are added for one field" {
let paramList = addFieldParams [ { Field.Between "that" "eh" "zed" with ParameterName = Some "@test" } ] []
let paramList = addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] []
Expect.hasLength paramList 2 "There should have been 2 parameters added"
let name, value = Seq.head paramList
Expect.equal name "@testmin" "Minimum field name not correct"
@@ -135,66 +135,48 @@ let parametersTests = testList "Parameters" [
/// Unit tests for the Query module of the PostgreSQL library
let queryTests = testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical comparison is passed" {
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields Any [ { Field.Greater "theField" "0" with ParameterName = Some "@test" } ])
(Query.whereByFields Any [ { Field.GT "theField" "0" with ParameterName = Some "@test" } ])
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence comparison is passed" {
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields Any [ Field.NotExists "thatField" ])
(Query.whereByFields Any [ Field.NEX "thatField" ])
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between comparison is passed with numeric values" {
test "succeeds for a single field when a between operator is passed with numeric values" {
Expect.equal
(Query.whereByFields All [ { Field.Between "aField" 50 99 with ParameterName = Some "@range" } ])
(Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ])
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for a single field when a between comparison is passed with non-numeric values" {
test "succeeds for a single field when a between operator is passed with non-numeric values" {
Expect.equal
(Query.whereByFields Any [ { Field.Between "field0" "a" "b" with ParameterName = Some "@alpha" } ])
(Query.whereByFields Any [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ])
"data->>'field0' BETWEEN @alphamin AND @alphamax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical comparisons" {
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields All [ Field.Equal "theFirst" "1"; Field.Equal "numberTwo" "2" ])
(Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ])
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence comparisons" {
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields Any [ Field.NotExists "thatField"; Field.GreaterOrEqual "thisField" 18 ])
(Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ])
"data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between comparisons" {
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields All [ Field.Between "aField" 50 99; Field.Between "anotherField" "a" "b" ])
(Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ])
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
test "succeeds for a field with an In comparison with alphanumeric values" {
Expect.equal
(Query.whereByFields All [ Field.In "this" [ "a"; "b"; "c" ] ])
"data->>'this' IN (@field0_0, @field0_1, @field0_2)"
"WHERE clause not correct"
}
test "succeeds for a field with an In comparison with numeric values" {
Expect.equal
(Query.whereByFields All [ Field.In "this" [ 7; 14; 21 ] ])
"(data->>'this')::numeric IN (@field0_0, @field0_1, @field0_2)"
"WHERE clause not correct"
}
test "succeeds for a field with an InArray comparison" {
Expect.equal
(Query.whereByFields All [ Field.InArray "theField" "the_table" [ "q", "r" ] ])
"data->'theField' ?| @field0"
"WHERE clause not correct"
}
]
testList "whereById" [
test "succeeds for numeric ID" {
@@ -252,7 +234,7 @@ let queryTests = testList "Query" [
}
test "byFields succeeds" {
Expect.equal
(Query.byFields "unit" Any [ Field.Greater "That" 14 ])
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE (data->>'That')::numeric > @field0"
"By-Field query not correct"
}
@@ -270,9 +252,18 @@ let queryTests = testList "Query" [
open ThrowawayDb.Postgres
open Types
/// Documents to use for integration tests
let documents = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]
/// Load the test documents into the database
let loadDocs () = backgroundTask {
for doc in testDocuments do do! insert PostgresDb.TableName doc
for doc in documents do do! insert PostgresDb.TableName doc
}
/// Integration tests for the Configuration module of the PostgreSQL library
@@ -444,68 +435,6 @@ let documentTests = testList "Document" [
|> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
testTask "succeeds when adding a numeric auto ID" {
try
Configuration.useAutoIdStrategy Number
Configuration.useIdField "Key"
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should be no documents in the table"
do! insert PostgresDb.TableName { Key = 0; Text = "one" }
do! insert PostgresDb.TableName { Key = 0; Text = "two" }
do! insert PostgresDb.TableName { Key = 77; Text = "three" }
do! insert PostgresDb.TableName { Key = 0; Text = "four" }
let! after = Find.allOrdered<NumIdDocument> PostgresDb.TableName [ Field.Named "n:Key" ]
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly"
finally
Configuration.useAutoIdStrategy Disabled
Configuration.useIdField "Id"
}
testTask "succeeds when adding a GUID auto ID" {
try
Configuration.useAutoIdStrategy Guid
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should be no documents in the table"
do! insert PostgresDb.TableName { emptyDoc with Value = "one" }
do! insert PostgresDb.TableName { emptyDoc with Value = "two" }
do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" }
do! insert PostgresDb.TableName { emptyDoc with Value = "four" }
let! after = Find.all<JsonDocument> PostgresDb.TableName
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs"
Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is"
finally
Configuration.useAutoIdStrategy Disabled
}
testTask "succeeds when adding a RandomString auto ID" {
try
Configuration.useAutoIdStrategy RandomString
Configuration.useIdStringLength 44
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should be no documents in the table"
do! insert PostgresDb.TableName { emptyDoc with Value = "one" }
do! insert PostgresDb.TableName { emptyDoc with Value = "two" }
do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" }
do! insert PostgresDb.TableName { emptyDoc with Value = "four" }
let! after = Find.all<JsonDocument> PostgresDb.TableName
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.hasCountOf
after 3u (fun doc -> doc.Id.Length = 44)
"Three of the IDs should have been 44-character random strings"
Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is"
finally
Configuration.useAutoIdStrategy Disabled
Configuration.useIdStringLength 16
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
@@ -550,15 +479,14 @@ let countTests = testList "Count" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount =
Count.byFields PostgresDb.TableName Any [ Field.Between "NumValue" 15 20; Field.Equal "NumValue" 0 ]
let! theCount = Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ]
Expect.equal theCount 3 "There should have been 3 matching documents"
}
testTask "succeeds when items are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byFields PostgresDb.TableName All [ Field.Exists "Sub"; Field.Greater "NumValue" 100 ]
let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ]
Expect.equal theCount 0 "There should have been no matching documents"
}
]
@@ -601,14 +529,14 @@ let existsTests = testList "Exists" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byFields PostgresDb.TableName Any [ Field.Exists "Sub"; Field.Exists "Boo" ]
let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ]
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byFields PostgresDb.TableName All [ Field.Equal "NumValue" "six"; Field.Exists "Nope" ]
let! exists = Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ]
Expect.isFalse exists "There should not have been existing documents"
}
]
@@ -726,16 +654,8 @@ let findTests = testList "Find" [
do! loadDocs ()
let! docs =
Find.byFields<JsonDocument>
PostgresDb.TableName All [ Field.In "Value" [ "purple"; "blue" ]; Field.Exists "Sub" ]
Expect.hasLength docs 1 "There should have been one document returned"
}
testTask "succeeds when documents are found using IN with numeric field" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byFields<JsonDocument> PostgresDb.TableName All [ Field.In "NumValue" [ 2; 4; 6; 8 ] ]
Expect.hasLength docs 1 "There should have been one document returned"
Find.byFields<JsonDocument> PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ]
Expect.equal (List.length docs) 1 "There should have been one document returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
@@ -743,27 +663,7 @@ let findTests = testList "Find" [
let! docs =
Find.byFields<JsonDocument>
PostgresDb.TableName All [ Field.Equal "Value" "mauve"; Field.NotEqual "NumValue" 40 ]
Expect.isEmpty docs "There should have been no documents returned"
}
testTask "succeeds for InArray when matching documents exist" {
use db = PostgresDb.BuildDb()
do! Definition.ensureTable PostgresDb.TableName
for doc in ArrayDocument.TestDocuments do do! insert PostgresDb.TableName doc
let! docs =
Find.byFields<ArrayDocument>
PostgresDb.TableName All [ Field.InArray "Values" PostgresDb.TableName [ "c" ] ]
Expect.hasLength docs 2 "There should have been two documents returned"
}
testTask "succeeds for InArray when no matching documents exist" {
use db = PostgresDb.BuildDb()
do! Definition.ensureTable PostgresDb.TableName
for doc in ArrayDocument.TestDocuments do do! insert PostgresDb.TableName doc
let! docs =
Find.byFields<ArrayDocument>
PostgresDb.TableName All [ Field.InArray "Values" PostgresDb.TableName [ "j" ] ]
PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ]
Expect.isEmpty docs "There should have been no documents returned"
}
]
@@ -774,7 +674,7 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "Value" "purple" ] [ Field.Named "Id" ]
PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly"
@@ -785,7 +685,7 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "Value" "purple" ] [ Field.Named "Id DESC" ]
PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly"
@@ -878,7 +778,7 @@ let findTests = testList "Find" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "another" ]
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "another" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
@@ -886,7 +786,7 @@ let findTests = testList "Find" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "purple" ]
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned"
}
@@ -894,7 +794,7 @@ let findTests = testList "Find" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "absent" ]
let! doc = Find.firstByFields<JsonDocument> PostgresDb.TableName Any [ Field.EQ "Value" "absent" ]
Expect.isNone doc "There should not have been a document returned"
}
]
@@ -905,7 +805,7 @@ let findTests = testList "Find" [
let! doc =
Find.firstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] [ Field.Named "Id" ]
PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "five" doc.Value.Id "An incorrect document was returned"
}
@@ -915,7 +815,7 @@ let findTests = testList "Find" [
let! doc =
Find.firstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] [ Field.Named "Id DESC" ]
PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "four" doc.Value.Id "An incorrect document was returned"
}
@@ -1092,8 +992,8 @@ let patchTests = testList "Patch" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Patch.byFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = Count.byFields PostgresDb.TableName Any [ Field.Equal "NumValue" 77 ]
do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |}
let! after = Count.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" 77 ]
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
@@ -1103,7 +1003,7 @@ let patchTests = testList "Patch" [
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byFields PostgresDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |}
}
]
testList "byContains" [
@@ -1154,9 +1054,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -1164,9 +1064,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -1188,20 +1088,20 @@ let removeFieldsTests = testList "RemoveFields" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Sub"; "Value" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -1209,13 +1109,13 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
// This not raising an exception is the test
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Nothing" ]
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ]
}
testTask "succeeds when no document is matched" {
use db = PostgresDb.BuildDb()
// This not raising an exception is the test
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ]
}
]
testList "byContains" [
@@ -1224,9 +1124,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -1234,9 +1134,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -1259,9 +1159,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields"
}
testTask "succeeds when a single field is removed" {
@@ -1269,9 +1169,9 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields"
}
testTask "succeeds when a field is not removed" {
@@ -1315,7 +1215,7 @@ let deleteTests = testList "Delete" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ]
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
@@ -1323,7 +1223,7 @@ let deleteTests = testList "Delete" [
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byFields PostgresDb.TableName Any [ Field.Equal "Value" "crimson" ]
do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ]
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}

View File

@@ -118,7 +118,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! theCount = conn.countByFields SqliteDb.TableName Any [ Field.Equal "Value" "purple" ]
let! theCount = conn.countByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ]
Expect.equal theCount 2L "There should have been 2 matching documents"
}
testList "existsById" [
@@ -145,7 +145,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 10 ]
let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ]
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
@@ -153,7 +153,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.Equal "Nothing" "none" ]
let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "Nothing" "none" ]
Expect.isFalse exists "There should not have been any existing documents"
}
]
@@ -244,7 +244,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ]
let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ]
Expect.hasLength docs 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
@@ -252,7 +252,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "mauve" ]
let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Value" "mauve" ]
Expect.isEmpty docs "There should have been no documents returned"
}
]
@@ -264,7 +264,7 @@ let integrationTests =
let! docs =
conn.findByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id" ]
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly"
}
@@ -275,7 +275,7 @@ let integrationTests =
let! docs =
conn.findByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id DESC" ]
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly"
}
@@ -286,7 +286,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "another" ]
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Value" "another" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
@@ -295,7 +295,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ]
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ]
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
@@ -304,7 +304,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "absent" ]
let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Value" "absent" ]
Expect.isNone doc "There should not have been a document returned"
}
]
@@ -316,7 +316,7 @@ let integrationTests =
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ]
SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "two" doc.Value.Id "An incorrect document was returned"
}
@@ -327,7 +327,7 @@ let integrationTests =
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ]
SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "four" doc.Value.Id "An incorrect document was returned"
}
@@ -416,8 +416,8 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.patchByFields SqliteDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 77 ]
do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ]
Expect.equal after 2L "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
@@ -428,7 +428,7 @@ let integrationTests =
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByFields SqliteDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |}
}
]
testList "removeFieldsById" [
@@ -467,7 +467,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Sub" ]
do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ]
try
let! _ = conn.findById<string, JsonDocument> SqliteDb.TableName "four"
Expect.isTrue false "The updated document should have failed to parse"
@@ -481,15 +481,14 @@ let integrationTests =
do! loadDocs ()
// This not raising an exception is the test
do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Nothing" ]
do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ]
}
testTask "succeeds when no document is matched" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
// This not raising an exception is the test
do! conn.removeFieldsByFields
SqliteDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ]
}
]
testList "deleteById" [
@@ -518,7 +517,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteByFields SqliteDb.TableName Any [ Field.NotEqual "Value" "purple" ]
do! conn.deleteByFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ]
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 2L "There should have been 2 documents remaining"
}
@@ -527,7 +526,7 @@ let integrationTests =
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteByFields SqliteDb.TableName Any [ Field.Equal "Value" "crimson" ]
do! conn.deleteByFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ]
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}

View File

@@ -15,57 +15,45 @@ open Types
/// Unit tests for the Query module of the SQLite library
let queryTests = testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical comparison is passed" {
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields Any [ { Field.Greater "theField" 0 with ParameterName = Some "@test" } ])
(Query.whereByFields Any [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ])
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence comparison is passed" {
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields Any [ Field.NotExists "thatField" ])
(Query.whereByFields Any [ Field.NEX "thatField" ])
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between comparison is passed" {
test "succeeds for a single field when a between operator is passed" {
Expect.equal
(Query.whereByFields All [ { Field.Between "aField" 50 99 with ParameterName = Some "@range" } ])
(Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ])
"data->>'aField' BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical comparisons" {
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields All [ Field.Equal "theFirst" "1"; Field.Equal "numberTwo" "2" ])
(Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ])
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence comparison" {
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields Any [ Field.NotExists "thatField"; Field.GreaterOrEqual "thisField" 18 ])
(Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ])
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between comparisons" {
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields All [ Field.Between "aField" 50 99; Field.Between "anotherField" "a" "b" ])
(Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ])
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
test "succeeds for a field with an In comparison" {
Expect.equal
(Query.whereByFields All [ Field.In "this" [ "a"; "b"; "c" ] ])
"data->>'this' IN (@field0_0, @field0_1, @field0_2)"
"WHERE clause not correct"
}
test "succeeds for a field with an InArray comparison" {
Expect.equal
(Query.whereByFields All [ Field.InArray "this" "the_table" [ "a"; "b" ] ])
"EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (@field0_0, @field0_1))"
"WHERE clause not correct"
}
]
test "whereById succeeds" {
Expect.equal (Query.whereById "abc") "data->>'Id' = @id" "WHERE clause not correct"
Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct"
}
test "patch succeeds" {
Expect.equal
@@ -84,7 +72,7 @@ let queryTests = testList "Query" [
}
test "byFields succeeds" {
Expect.equal
(Query.byFields "unit" Any [ Field.Greater "That" 14 ])
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE data->>'That' > @field0"
"By-Field query not correct"
}
@@ -110,14 +98,14 @@ let parametersTests = testList "Parameters" [
}
testList "addFieldParam" [
test "succeeds when adding a parameter" {
let paramList = addFieldParam "@field" (Field.Equal "it" 99) []
let paramList = addFieldParam "@field" (Field.EQ "it" 99) []
Expect.hasLength paramList 1 "There should have been a parameter added"
let theParam = Seq.head paramList
Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect"
Expect.equal theParam.Value 99 "The parameter value is incorrect"
}
test "succeeds when not adding a parameter" {
let paramList = addFieldParam "@it" (Field.NotExists "Coffee") []
let paramList = addFieldParam "@it" (Field.NEX "Coffee") []
Expect.isEmpty paramList "There should not have been any parameters added"
}
]
@@ -130,9 +118,18 @@ let parametersTests = testList "Parameters" [
(** INTEGRATION TESTS **)
/// Documents used for integration tests
let documents = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]
/// Load a table with the test documents
let loadDocs () = backgroundTask {
for doc in testDocuments do do! insert SqliteDb.TableName doc
for doc in documents do do! insert SqliteDb.TableName doc
}
/// Integration tests for the Configuration module of the SQLite library
@@ -147,6 +144,32 @@ let configurationTests = testList "Configuration" [
finally
Configuration.useConnectionString "Data Source=:memory:"
}
test "useSerializer succeeds" {
try
Configuration.useSerializer
{ new IDocumentSerializer with
member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}"""
member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T>
}
let serialized = Configuration.serializer().Serialize { Foo = "howdy"; Bar = "bye"}
Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used"
let deserialized = Configuration.serializer().Deserialize<obj> """{"Something":"here"}"""
Expect.isNull deserialized "Specified serializer should have returned null"
finally
Configuration.useSerializer DocumentSerializer.``default``
}
test "serializer returns configured serializer" {
Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ()))
"Serializer should have been the same"
}
test "useIdField / idField succeeds" {
Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect"
Configuration.useIdField "id"
Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field"
Configuration.useIdField "Id"
}
]
/// Integration tests for the Custom module of the SQLite library
@@ -286,68 +309,6 @@ let documentTests = testList "Document" [
insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
testTask "succeeds when adding a numeric auto ID" {
try
Configuration.useAutoIdStrategy Number
Configuration.useIdField "Key"
use! db = SqliteDb.BuildDb()
let! before = Count.all SqliteDb.TableName
Expect.equal before 0L "There should be no documents in the table"
do! insert SqliteDb.TableName { Key = 0; Text = "one" }
do! insert SqliteDb.TableName { Key = 0; Text = "two" }
do! insert SqliteDb.TableName { Key = 77; Text = "three" }
do! insert SqliteDb.TableName { Key = 0; Text = "four" }
let! after = Find.allOrdered<NumIdDocument> SqliteDb.TableName [ Field.Named "Key" ]
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly"
finally
Configuration.useAutoIdStrategy Disabled
Configuration.useIdField "Id"
}
testTask "succeeds when adding a GUID auto ID" {
try
Configuration.useAutoIdStrategy Guid
use! db = SqliteDb.BuildDb()
let! before = Count.all SqliteDb.TableName
Expect.equal before 0L "There should be no documents in the table"
do! insert SqliteDb.TableName { emptyDoc with Value = "one" }
do! insert SqliteDb.TableName { emptyDoc with Value = "two" }
do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" }
do! insert SqliteDb.TableName { emptyDoc with Value = "four" }
let! after = Find.all<JsonDocument> SqliteDb.TableName
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs"
Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is"
finally
Configuration.useAutoIdStrategy Disabled
}
testTask "succeeds when adding a RandomString auto ID" {
try
Configuration.useAutoIdStrategy RandomString
Configuration.useIdStringLength 44
use! db = SqliteDb.BuildDb()
let! before = Count.all SqliteDb.TableName
Expect.equal before 0L "There should be no documents in the table"
do! insert SqliteDb.TableName { emptyDoc with Value = "one" }
do! insert SqliteDb.TableName { emptyDoc with Value = "two" }
do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" }
do! insert SqliteDb.TableName { emptyDoc with Value = "four" }
let! after = Find.all<JsonDocument> SqliteDb.TableName
Expect.hasLength after 4 "There should have been 4 documents returned"
Expect.hasCountOf
after 3u (fun doc -> doc.Id.Length = 44)
"Three of the IDs should have been 44-character random strings"
Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is"
finally
Configuration.useAutoIdStrategy Disabled
Configuration.useIdStringLength 16
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
@@ -392,14 +353,14 @@ let countTests = testList "Count" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byFields SqliteDb.TableName Any [ Field.Between "NumValue" 10 20 ]
let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "NumValue" 10 20 ]
Expect.equal theCount 3L "There should have been 3 matching documents"
}
testTask "succeeds for a non-numeric range" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byFields SqliteDb.TableName Any [ Field.Between "Value" "aardvark" "apple" ]
let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "Value" "aardvark" "apple" ]
Expect.equal theCount 1L "There should have been 1 matching document"
}
]
@@ -428,14 +389,14 @@ let existsTests = testList "Exists" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byFields SqliteDb.TableName Any [ Field.Equal "NumValue" 10 ]
let! exists = Exists.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ]
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byFields SqliteDb.TableName Any [ Field.Less "Nothing" "none" ]
let! exists = Exists.byFields SqliteDb.TableName Any [ Field.LT "Nothing" "none" ]
Expect.isFalse exists "There should not have been any existing documents"
}
]
@@ -520,43 +481,16 @@ let findTests = testList "Find" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Find.byFields<JsonDocument> SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ]
Expect.hasLength docs 2 "There should have been two documents returned"
}
testTask "succeeds when documents are found using IN with numeric field" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Find.byFields<JsonDocument> SqliteDb.TableName All [ Field.In "NumValue" [ 2; 4; 6; 8 ] ]
Expect.hasLength docs 1 "There should have been one document returned"
let! docs = Find.byFields<JsonDocument> SqliteDb.TableName Any [ Field.GT "NumValue" 15 ]
Expect.equal (List.length docs) 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Find.byFields<JsonDocument> SqliteDb.TableName Any [ Field.Greater "NumValue" 100 ]
let! docs = Find.byFields<JsonDocument> SqliteDb.TableName Any [ Field.GT "NumValue" 100 ]
Expect.isTrue (List.isEmpty docs) "There should have been no documents returned"
}
testTask "succeeds for InArray when matching documents exist" {
use! db = SqliteDb.BuildDb()
do! Definition.ensureTable SqliteDb.TableName
for doc in ArrayDocument.TestDocuments do do! insert SqliteDb.TableName doc
let! docs =
Find.byFields<ArrayDocument>
SqliteDb.TableName All [ Field.InArray "Values" SqliteDb.TableName [ "c" ] ]
Expect.hasLength docs 2 "There should have been two documents returned"
}
testTask "succeeds for InArray when no matching documents exist" {
use! db = SqliteDb.BuildDb()
do! Definition.ensureTable SqliteDb.TableName
for doc in ArrayDocument.TestDocuments do do! insert SqliteDb.TableName doc
let! docs =
Find.byFields<ArrayDocument>
SqliteDb.TableName All [ Field.InArray "Values" SqliteDb.TableName [ "j" ] ]
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "byFieldsOrdered" [
testTask "succeeds when sorting ascending" {
@@ -565,8 +499,7 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id" ]
Expect.hasLength docs 2 "There should have been two documents returned"
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly"
}
@@ -576,40 +509,17 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id DESC" ]
Expect.hasLength docs 2 "There should have been two documents returned"
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly"
}
testTask "succeeds when sorting case-sensitively" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName All [ Field.LessOrEqual "NumValue" 10 ] [ Field.Named "Value" ]
Expect.hasLength docs 3 "There should have been three documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "three|one|two" "Documents not ordered correctly"
}
testTask "succeeds when sorting case-insensitively" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName All [ Field.LessOrEqual "NumValue" 10 ] [ Field.Named "i:Value" ]
Expect.hasLength docs 3 "There should have been three documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "three|two|one" "Documents not ordered correctly"
}
]
testList "firstByFields" [
testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "another" ]
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Value" "another" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
@@ -617,7 +527,7 @@ let findTests = testList "Find" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ]
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ]
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
@@ -625,7 +535,7 @@ let findTests = testList "Find" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "absent" ]
let! doc = Find.firstByFields<JsonDocument> SqliteDb.TableName Any [ Field.EQ "Value" "absent" ]
Expect.isNone doc "There should not have been a document returned"
}
]
@@ -636,7 +546,7 @@ let findTests = testList "Find" [
let! doc =
Find.firstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ]
SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "two" doc.Value.Id "An incorrect document was returned"
}
@@ -646,7 +556,7 @@ let findTests = testList "Find" [
let! doc =
Find.firstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ]
SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "four" doc.Value.Id "An incorrect document was returned"
}
@@ -729,8 +639,8 @@ let patchTests = testList "Patch" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Patch.byFields SqliteDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = Count.byFields SqliteDb.TableName Any [ Field.Equal "NumValue" 77 ]
do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |}
let! after = Count.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ]
Expect.equal after 2L "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
@@ -740,7 +650,7 @@ let patchTests = testList "Patch" [
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byFields SqliteDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |}
}
]
]
@@ -779,7 +689,7 @@ let removeFieldsTests = testList "RemoveFields" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Sub" ]
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ]
try
let! _ = Find.byId<string, JsonDocument> SqliteDb.TableName "four"
Expect.isTrue false "The updated document should have failed to parse"
@@ -792,13 +702,13 @@ let removeFieldsTests = testList "RemoveFields" [
do! loadDocs ()
// This not raising an exception is the test
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Nothing" ]
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ]
}
testTask "succeeds when no document is matched" {
use! db = SqliteDb.BuildDb()
// This not raising an exception is the test
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ]
}
]
]
@@ -828,7 +738,7 @@ let deleteTests = testList "Delete" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byFields SqliteDb.TableName Any [ Field.NotEqual "Value" "purple" ]
do! Delete.byFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ]
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 2L "There should have been 2 documents remaining"
}
@@ -836,7 +746,7 @@ let deleteTests = testList "Delete" [
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byFields SqliteDb.TableName Any [ Field.Equal "Value" "crimson" ]
do! Delete.byFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ]
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}

View File

@@ -1,39 +1,23 @@
module Types
type NumIdDocument =
{ Key: int
Text: string }
type SubDocument =
{ Foo: string
Bar: string }
type ArrayDocument =
{ Id: string
Values: string list }
with
/// <summary>
/// A set of documents used for integration tests
/// </summary>
static member TestDocuments =
[ { Id = "first"; Values = [ "a"; "b"; "c" ] }
{ Id = "second"; Values = [ "c"; "d"; "e" ] }
{ Id = "third"; Values = [ "x"; "y"; "z" ] } ]
type JsonDocument =
{ Id: string
Value: string
NumValue: int
Sub: SubDocument option }
/// An empty JsonDocument
let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None }
/// Documents to use for testing
let testDocuments =
[ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None } ]
let testDocuments = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]

View File

@@ -1,16 +1,13 @@
#!/bin/bash
echo --- Package Common library
rm Common/bin/Release/BitBadger.Documents.Common.*.nupkg || true
dotnet pack Common/BitBadger.Documents.Common.fsproj -c Release
cp Common/bin/Release/BitBadger.Documents.Common.*.nupkg .
echo --- Package PostgreSQL library
rm Postgres/bin/Release/BitBadger.Documents.Postgres*.nupkg || true
dotnet pack Postgres/BitBadger.Documents.Postgres.fsproj -c Release
cp Postgres/bin/Release/BitBadger.Documents.Postgres.*.nupkg .
echo --- Package SQLite library
rm Sqlite/bin/Release/BitBadger.Documents.Sqlite*.nupkg || true
dotnet pack Sqlite/BitBadger.Documents.Sqlite.fsproj -c Release
cp Sqlite/bin/Release/BitBadger.Documents.Sqlite.*.nupkg .

View File

@@ -7,8 +7,8 @@ dotnet build BitBadger.Documents.sln --no-restore
cd ./Tests || exit
export BBDOX_PG_PORT=8301
PG_VERSIONS=('13' '14' '15' '16' 'latest')
NET_VERSIONS=('8.0' '9.0')
PG_VERSIONS=('12' '13' '14' '15' 'latest')
NET_VERSIONS=('6.0' '8.0')
for PG_VERSION in "${PG_VERSIONS[@]}"
do