Compare commits

..

7 Commits
v3.1 ... main

Author SHA1 Message Date
740767661c Make Field constructor functions generic (#8)
F# can upcast types to `obj` if those types are used in place. However, a `string seq` (`IEnumerable<string>` in C#) cannot be upcast to an `obj seq` (`IEnumerable<object>`) without mapping each item in the sequence. Making the `Field` constructor functions generic will allow them to take any object type, and these functions handle the conversion to `obj` (for `In` and `InArray`; others work transparently).

Reviewed-on: #8
2024-09-18 13:36:14 +00:00
168bf0cd14 RC4 changes (#7)
- Add `In` and `InArray` comparisons
- Replace `Op` with `Comparison` (internal API, but was public)
- Spell out comparisons in `Field` constructor functions

Reviewed-on: #7
2024-09-17 02:33:57 +00:00
3bc662c984 Preserve additional ORDER BY qualifiers
- Bump version to v4-rc3
2024-08-22 20:26:37 -04:00
b019548a4e Bump version to v4-rc2 2024-08-21 21:13:40 -04:00
27b8a83a7a Add case-insensitive ordering 2024-08-21 21:03:38 -04:00
2c24e2e912 Version 4 rc1 (#6)
Changes in this version:
- **BREAKING CHANGE**: All `*byField`/`*ByField` functions are now `*byFields`/`*ByFields`, and take a `FieldMatch` case before the list of fields. The `Compat` namespace in both libraries will assist in this transition. In support of this change, the `Field` parameter name is optional; the library will generate parameter names for it if they are not specified.
- **BREAKING CHANGE**: The `Query` namespaces have had some significant work, particularly from the full-query perspective. Most have been broken up into the base query and modifiers `by*` that will combine the base query with the `WHERE` clause needed to satisfy the criteria.
- **FEATURE / BREAKING CHANGE**: PostgreSQL document fields will now be cast to numeric if the parameter value passed to the query is numeric. This drove the `Query` breaking changes, as the fields need to have their intended value for the library to generate the appropriate SQL. Additionally, if code assumes the library can be given something like `8` and transform it to `"8"`, this is no longer the case.
- **FEATURE**: All `Find` queries (except `byId`/`ById`) now have a version with the `Ordered` suffix. These take a list of fields by which the query should be ordered. A new `Field` method called `Named` can assist with creating these fields. Prefixing the field name with `n:` will cast the field to numeric in PostgreSQL (and will be ignored by SQLite); adding " DESC" to the field name will sort it descending (Z-A, high to low) instead of ascending (A-Z, low to high).
- **BREAKING CHANGE** (PostgreSQL only): `fieldNameParam`/`Parameters.FieldName` are now plural. The function still only generates one parameter, but the name is now the same between PostgreSQL and SQLite. The goal of this library is to abstract the differences away as much as practical, and this furthers that end. There are functions with these names in the `Compat` namespace.
- **FEATURE**: In the F# v3 library, lists of parameters were expected to be F#'s `List` type, and the C# version took either `List<T>` or `IEnumerable<T>`. In this version, these all expect `seq`/`IEnumerable<T>`. F#'s `List` satisfies the `seq` constraints, so this should not be a breaking change.
- **FEATURE**: `Field`s now may have qualifiers; this allows tables to be aliased when joining multiple tables (as all have the same `data` column). F# users can use `with` to specify this at creation, and both F# and C# can use the `WithQualifier` method to create a field with the qualifier specified. Parameter names for fields may be specified in a similar way, substituting `ParameterName` for `Qualifier`.

Reviewed-on: #6
2024-08-19 23:30:38 +00:00
039761fcca Remove project-specific release notes 2024-06-05 22:10:48 -04:00
30 changed files with 8632 additions and 5009 deletions

4
.gitignore vendored
View File

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

View File

@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<Description>Common files for PostgreSQL and SQLite document database libraries</Description> <Description>Common files for PostgreSQL and SQLite document database libraries</Description>
<PackageReleaseNotes>v3 release</PackageReleaseNotes>
<PackageTags>JSON Document SQL</PackageTags> <PackageTags>JSON Document SQL</PackageTags>
</PropertyGroup> </PropertyGroup>

View File

@ -1,87 +1,281 @@
namespace BitBadger.Documents namespace BitBadger.Documents
/// The types of logical operations available for JSON fields open System.Security.Cryptography
[<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
override this.ToString() = /// The types of comparisons available for JSON fields
type Comparison =
/// Equals (=)
| Equal of Value: obj
/// Greater Than (>)
| Greater of Value: obj
/// Greater Than or Equal To (>=)
| GreaterOrEqual of Value: obj
/// Less Than (<)
| Less of Value: obj
/// Less Than or Equal To (<=)
| LessOrEqual of Value: obj
/// Not Equal to (<>)
| NotEqual of Value: obj
/// Between (BETWEEN)
| Between of Min: obj * Max: obj
/// In (IN)
| In of Values: obj seq
/// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
| InArray of Table: string * Values: obj seq
/// Exists (IS NOT NULL)
| Exists
/// Does Not Exist (IS NULL)
| NotExists
/// Get the operator SQL for this comparison
member this.OpSql =
match this with match this with
| EQ -> "=" | Equal _ -> "="
| GT -> ">" | Greater _ -> ">"
| GE -> ">=" | GreaterOrEqual _ -> ">="
| LT -> "<" | Less _ -> "<"
| LE -> "<=" | LessOrEqual _ -> "<="
| NE -> "<>" | NotEqual _ -> "<>"
| BT -> "BETWEEN" | Between _ -> "BETWEEN"
| EX -> "IS NOT NULL" | In _ -> "IN"
| NEX -> "IS NULL" | InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
| Exists -> "IS NOT NULL"
| NotExists -> "IS NULL"
/// The dialect in which a command should be rendered
[<Struct>]
type Dialect =
| PostgreSQL
| SQLite
/// The format in which an element of a JSON field should be extracted
[<Struct>]
type FieldFormat =
/// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
| AsSql
/// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
| AsJson
/// Criteria for a field WHERE clause /// Criteria for a field WHERE clause
type Field = { type Field =
/// The name of the field { /// The name of the field
Name: string Name: string
/// The operation by which the field will be compared /// The comparison for the field
Op: Op Comparison: Comparison
/// The value of the field /// The name of the parameter for this field
Value: obj ParameterName: string option
} with
/// The table qualifier for this field
Qualifier: string option }
with
/// Create a comparison against a field
static member Where name (comparison: Comparison) =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
/// Create an equals (=) field criterion /// Create an equals (=) field criterion
static member EQ name (value: obj) = static member Equal<'T> name (value: 'T) =
{ Name = name; Op = EQ; Value = value } Field.Where name (Equal value)
/// Create an equals (=) field criterion (alias)
static member EQ<'T> name (value: 'T) = Field.Equal name value
/// Create a greater than (>) field criterion /// Create a greater than (>) field criterion
static member GT name (value: obj) = static member Greater<'T> name (value: 'T) =
{ Name = name; Op = GT; Value = value } Field.Where name (Greater value)
/// Create a greater than (>) field criterion (alias)
static member GT<'T> name (value: 'T) = Field.Greater name value
/// Create a greater than or equal to (>=) field criterion /// Create a greater than or equal to (>=) field criterion
static member GE name (value: obj) = static member GreaterOrEqual<'T> name (value: 'T) =
{ Name = name; Op = GE; Value = value } Field.Where name (GreaterOrEqual value)
/// Create a greater than or equal to (>=) field criterion (alias)
static member GE<'T> name (value: 'T) = Field.GreaterOrEqual name value
/// Create a less than (<) field criterion /// Create a less than (<) field criterion
static member LT name (value: obj) = static member Less<'T> name (value: 'T) =
{ Name = name; Op = LT; Value = value } Field.Where name (Less value)
/// Create a less than (<) field criterion (alias)
static member LT<'T> name (value: 'T) = Field.Less name value
/// Create a less than or equal to (<=) field criterion /// Create a less than or equal to (<=) field criterion
static member LE name (value: obj) = static member LessOrEqual<'T> name (value: 'T) =
{ Name = name; Op = LE; Value = value } Field.Where name (LessOrEqual value)
/// Create a less than or equal to (<=) field criterion (alias)
static member LE<'T> name (value: 'T) = Field.LessOrEqual name value
/// Create a not equals (<>) field criterion /// Create a not equals (<>) field criterion
static member NE name (value: obj) = static member NotEqual<'T> name (value: 'T) =
{ Name = name; Op = NE; Value = value } Field.Where name (NotEqual value)
/// Create a BETWEEN field criterion /// Create a not equals (<>) field criterion (alias)
static member BT name (min: obj) (max: obj) = static member NE<'T> name (value: 'T) = Field.NotEqual name value
{ Name = name; Op = BT; Value = [ min; max ] }
/// Create a Between field criterion
static member Between<'T> name (min: 'T) (max: 'T) =
Field.Where name (Between(min, max))
/// Create a Between field criterion (alias)
static member BT<'T> name (min: 'T) (max: 'T) = Field.Between name min max
/// Create an In field criterion
static member In<'T> name (values: 'T seq) =
Field.Where name (In (Seq.map box values))
/// Create an In field criterion (alias)
static member IN<'T> name (values: 'T seq) = Field.In name values
/// Create an InArray field criterion
static member InArray<'T> name tableName (values: 'T seq) =
Field.Where name (InArray(tableName, Seq.map box values))
/// Create an exists (IS NOT NULL) field criterion /// Create an exists (IS NOT NULL) field criterion
static member EX name = static member Exists name =
{ Name = name; Op = EX; Value = obj () } Field.Where name Exists
/// Create an exists (IS NOT NULL) field criterion (alias)
static member EX name = Field.Exists name
/// Create a not exists (IS NULL) field criterion /// Create a not exists (IS NULL) field criterion
static member NEX name = static member NotExists name =
{ Name = name; Op = NEX; Value = obj () } Field.Where name NotExists
/// Create a not exists (IS NULL) field criterion (alias)
static member NEX name = Field.NotExists name
/// Transform a field name (a.b.c) to a path for the given SQL dialect
static member NameToPath (name: string) dialect format =
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}'"
$"data{path}"
/// 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 "")
/// Specify the name of the parameter for this field
member this.WithParameterName name =
{ this with ParameterName = Some name }
/// Specify a qualifier (alias) for the table from which this field will be referenced
member this.WithQualifier alias =
{ this with Qualifier = Some alias }
/// Get the qualified path to the field
member this.Path dialect format =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "")
+ Field.NameToPath this.Name dialect format
/// How fields should be matched
[<Struct>]
type FieldMatch =
/// Any field matches (OR)
| Any
/// All fields match (AND)
| All
/// The SQL value implementing each matching strategy
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
type ParameterName() =
/// The counter for the next field value
let mutable currentIdx = -1
/// Return the specified name for the parameter, or an anonymous parameter name if none is specified
member this.Derive paramName =
match paramName with
| Some it -> it
| None ->
currentIdx <- currentIdx + 1
$"@field{currentIdx}"
#if NET6_0
open System.Text
#endif
/// Automatically-generated document ID strategies
[<Struct>]
type AutoId =
/// No automatic IDs will be generated
| Disabled
/// Generate a MAX-plus-1 numeric value for documents
| Number
/// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
| Guid
/// Generate a random string of hexadecimal characters for each document
| RandomString
with
/// Generate a GUID string
static member GenerateGuid () =
System.Guid.NewGuid().ToString "N"
/// Generate a string of random hexadecimal characters
static member GenerateRandomString (length: int) =
#if NET8_0_OR_GREATER
RandomNumberGenerator.GetHexString(length, lowercase = true)
#else
RandomNumberGenerator.GetBytes((length / 2) + 1)
|> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length)
|> function it -> it.Length <- length; it.ToString()
#endif
/// Does the given document need an automatic ID generated?
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
/// The required document serialization implementation /// The required document serialization implementation
@ -135,7 +329,7 @@ module Configuration =
serializerValue serializerValue
/// The serialized name of the ID field for documents /// The serialized name of the ID field for documents
let mutable idFieldValue = "Id" let mutable private idFieldValue = "Id"
/// Specify the name of the ID field for documents /// Specify the name of the ID field for documents
[<CompiledName "UseIdField">] [<CompiledName "UseIdField">]
@ -147,15 +341,41 @@ module Configuration =
let idField () = let idField () =
idFieldValue idFieldValue
/// The automatic ID strategy used by the library
let mutable private autoIdValue = Disabled
/// Specify the automatic ID generation strategy used by the library
[<CompiledName "UseAutoIdStrategy">]
let useAutoIdStrategy it =
autoIdValue <- it
/// Retrieve the currently configured automatic ID generation strategy
[<CompiledName "AutoIdStrategy">]
let autoIdStrategy () =
autoIdValue
/// The length of automatically generated random strings
let mutable private idStringLengthValue = 16
/// Specify the length of automatically generated random strings
[<CompiledName "UseIdStringLength">]
let useIdStringLength length =
idStringLengthValue <- length
/// Retrieve the currently configured length of automatically generated random strings
[<CompiledName "IdStringLength">]
let idStringLength () =
idStringLengthValue
/// Query construction functions /// Query construction functions
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Create a SELECT clause to retrieve the document data from the given table /// Combine a query (select, update, etc.) and a WHERE clause
[<CompiledName "SelectFromTable">] [<CompiledName "StatementWhere">]
let selectFromTable tableName = let statementWhere statement where =
$"SELECT data FROM %s{tableName}" $"%s{statement} WHERE %s{where}"
/// Queries to define tables and indexes /// Queries to define tables and indexes
module Definition = module Definition =
@ -172,7 +392,7 @@ module Query =
/// SQL statement to create an index on one or more fields in a JSON document /// SQL statement to create an index on one or more fields in a JSON document
[<CompiledName "EnsureIndexOn">] [<CompiledName "EnsureIndexOn">]
let ensureIndexOn tableName indexName (fields: string seq) = let ensureIndexOn tableName indexName (fields: string seq) dialect =
let _, tbl = splitSchemaAndTable tableName let _, tbl = splitSchemaAndTable tableName
let jsonFields = let jsonFields =
fields fields
@ -180,14 +400,14 @@ module Query =
let parts = it.Split ' ' let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0] let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}" let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"(data ->> '{fieldName}'){direction}") $"({Field.NameToPath fieldName dialect AsSql}){direction}")
|> String.concat ", " |> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})" $"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
/// SQL statement to create a key index for a document table /// SQL statement to create a key index for a document table
[<CompiledName "EnsureKey">] [<CompiledName "EnsureKey">]
let ensureKey tableName = let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX") (ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
/// Query to insert a document /// Query to insert a document
[<CompiledName "Insert">] [<CompiledName "Insert">]
@ -201,3 +421,58 @@ module Query =
"INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ()) tableName (Configuration.idField ())
/// Query to count documents in a table (no WHERE clause)
[<CompiledName "Count">]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// 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"
/// Query to select documents from a table (no WHERE clause)
[<CompiledName "Find">]
let find tableName =
$"SELECT data FROM %s{tableName}"
/// Query to update a document (no WHERE clause)
[<CompiledName "Update">]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
/// Query to delete documents from a table (no WHERE clause)
[<CompiledName "Delete">]
let delete tableName =
$"DELETE FROM %s{tableName}"
/// 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
/// Create an ORDER BY clause for the given fields
[<CompiledName "OrderBy">]
let orderBy fields dialect =
if Seq.isEmpty fields then ""
else
fields
|> 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 " "}"""
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
|> function path -> path + defaultArg direction "")
|> String.concat ", "
|> function it -> $" ORDER BY {it}"

View File

@ -3,10 +3,20 @@
<TargetFrameworks>net6.0;net8.0</TargetFrameworks> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.1.0.0</AssemblyVersion> <AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>3.1.0.0</FileVersion> <FileVersion>4.0.0.0</FileVersion>
<VersionPrefix>3.1.0</VersionPrefix> <VersionPrefix>4.0.0</VersionPrefix>
<PackageReleaseNotes>Add BT (between) operator; drop .NET 7 support</PackageReleaseNotes> <VersionSuffix>rc5</VersionSuffix>
<PackageReleaseNotes>From v3.1: (see project site for breaking changes and compatibility)
- Change ByField to ByFields
- Support dot-access to nested document fields
- Add Find*Ordered functions/methods
Release Candidate Changes:
- from v4-rc4: Field construction functions are now generic.
- from v4-rc3: Add In/InArray field comparisons, revamp internal comparison handling.
- from v4-rc2: preserve additional ORDER BY qualifiers.
- from v4-rc1: add case-insensitive ordering.</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>

View File

@ -2,13 +2,13 @@
<PropertyGroup> <PropertyGroup>
<Description>Use PostgreSQL as a document database</Description> <Description>Use PostgreSQL as a document database</Description>
<PackageReleaseNotes>v3 release; official replacement for BitBadger.Npgsql.Documents</PackageReleaseNotes>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags> <PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Library.fs" /> <Compile Include="Library.fs" />
<Compile Include="Extensions.fs" /> <Compile Include="Extensions.fs" />
<Compile Include="Compat.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" /> <None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>

270
src/Postgres/Compat.fs Normal file
View File

@ -0,0 +1,270 @@
namespace BitBadger.Documents.Postgres.Compat
open BitBadger.Documents
open BitBadger.Documents.Postgres
[<AutoOpen>]
module Parameters =
/// Create a JSON field parameter
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams (F#) / AddFields (C#) instead ~ will be removed in v4.1">]
let addFieldParam name field parameters =
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FieldName">]
[<System.Obsolete "Use fieldNameParams (F#) / FieldNames (C#) instead ~ will be removed in v4.1">]
let fieldNameParam fieldNames =
fieldNameParams fieldNames
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
[<System.Obsolete "Use WhereByFields instead ~ will be removed in v4.1">]
let whereByField field paramName =
Query.whereByFields Any [ { field with ParameterName = Some paramName } ]
module WithProps =
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Count.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Exists.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field sqlProps =
WithProps.Find.byFields<'TDoc> tableName Any [ field ] sqlProps
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field, sqlProps) =
WithProps.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field sqlProps =
WithProps.Find.firstByFields<'TDoc> tableName Any [ field ] sqlProps
/// 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>(tableName, field, sqlProps) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) sqlProps =
WithProps.Patch.byFields tableName Any [ field ] patch sqlProps
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames sqlProps =
WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames sqlProps
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Delete.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Count.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Exists.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field =
Find.byFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field) =
Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field =
Find.firstByFields<'TDoc> tableName Any [ field ]
/// 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>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) =
Patch.byFields tableName Any [ field ] patch
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames =
RemoveFields.byFields tableName Any [ field ] fieldNames
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Delete.byFields tableName Any [ field ]
open Npgsql
/// F# Extensions for the NpgsqlConnection type
[<AutoOpen>]
module Extensions =
type NpgsqlConnection with
/// Count matching documents using a JSON field comparison query (->> =)
[<System.Obsolete "Use countByFields instead ~ will be removed in v4.1">]
member conn.countByField tableName field =
conn.countByFields tableName Any [ field ]
/// Determine if documents exist using a JSON field comparison query (->> =)
[<System.Obsolete "Use existsByFields instead ~ will be removed in v4.1">]
member conn.existsByField tableName field =
conn.existsByFields tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use findByFields instead ~ will be removed in v4.1">]
member conn.findByField<'TDoc> tableName field =
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<System.Obsolete "Use findFirstByFields instead ~ will be removed in v4.1">]
member conn.findFirstByField<'TDoc> tableName field =
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<System.Obsolete "Use patchByFields instead ~ will be removed in v4.1">]
member conn.patchByField tableName field (patch: 'TPatch) =
conn.patchByFields tableName Any [ field ] patch
/// Remove fields from documents via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead ~ will be removed in v4.1">]
member conn.removeFieldsByField tableName field fieldNames =
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// Delete documents by matching a JSON field comparison query (->> =)
[<System.Obsolete "Use deleteByFields instead ~ will be removed in v4.1">]
member conn.deleteByField tableName field =
conn.deleteByFields tableName Any [ field ]
open System.Runtime.CompilerServices
open Npgsql.FSharp
type NpgsqlConnectionCSharpCompatExtensions =
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use CountByFields instead ~ will be removed in v4.1">]
static member inline CountByField(conn, tableName, field) =
WithProps.Count.byFields tableName Any [ field ] (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead ~ will be removed in v4.1">]
static member inline ExistsByField(conn, tableName, field) =
WithProps.Exists.byFields tableName Any [ field ] (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use FindByFields instead ~ will be removed in v4.1">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithProps.Find.ByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn)
/// 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>(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 (->> =)
[<Extension>]
[<System.Obsolete "Use PatchByFields instead ~ will be removed in v4.1">]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) =
WithProps.Patch.byFields tableName Any [ field ] patch (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead ~ will be removed in v4.1">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead ~ will be removed in v4.1">]
static member inline DeleteByField(conn, tableName, field) =
WithProps.Delete.byFields tableName Any [ field ] (Sql.existingConnection conn)

View File

@ -50,8 +50,8 @@ module Extensions =
WithProps.Count.all tableName (Sql.existingConnection conn) WithProps.Count.all tableName (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =) /// Count matching documents using a JSON field comparison query (->> =)
member conn.countByField tableName field = member conn.countByFields tableName howMatched fields =
WithProps.Count.byField tableName field (Sql.existingConnection conn) WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Count matching documents using a JSON containment query (@>) /// Count matching documents using a JSON containment query (@>)
member conn.countByContains tableName criteria = member conn.countByContains tableName criteria =
@ -66,8 +66,8 @@ module Extensions =
WithProps.Exists.byId tableName docId (Sql.existingConnection conn) WithProps.Exists.byId tableName docId (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =) /// Determine if documents exist using a JSON field comparison query (->> =)
member conn.existsByField tableName field = member conn.existsByFields tableName howMatched fields =
WithProps.Exists.byField tableName field (Sql.existingConnection conn) WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Determine if documents exist using a JSON containment query (@>) /// Determine if documents exist using a JSON containment query (@>)
member conn.existsByContains tableName criteria = member conn.existsByContains tableName criteria =
@ -81,34 +81,68 @@ module Extensions =
member conn.findAll<'TDoc> tableName = member conn.findAll<'TDoc> tableName =
WithProps.Find.all<'TDoc> tableName (Sql.existingConnection conn) WithProps.Find.all<'TDoc> tableName (Sql.existingConnection conn)
/// Retrieve all documents in the given table ordered by the given fields in the document
member conn.findAllOrdered<'TDoc> tableName orderFields =
WithProps.Find.allOrdered<'TDoc> tableName orderFields (Sql.existingConnection conn)
/// Retrieve a document by its ID; returns None if not found /// Retrieve a document by its ID; returns None if not found
member conn.findById<'TKey, 'TDoc> tableName docId = member conn.findById<'TKey, 'TDoc> tableName docId =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn) WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =) /// Retrieve documents matching a JSON field comparison query (->> =)
member conn.findByField<'TDoc> tableName field = member conn.findByFields<'TDoc> tableName howMatched fields =
WithProps.Find.byField<'TDoc> tableName field (Sql.existingConnection conn) WithProps.Find.byFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the
/// document
member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.byFieldsOrdered<'TDoc>
tableName howMatched queryFields orderFields (Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>) /// Retrieve documents matching a JSON containment query (@>)
member conn.findByContains<'TDoc> tableName (criteria: obj) = member conn.findByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn) WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document
member conn.findByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn)
/// Retrieve documents matching a JSON Path match query (@?) /// Retrieve documents matching a JSON Path match query (@?)
member conn.findByJsonPath<'TDoc> tableName jsonPath = member conn.findByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn)
/// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document
member conn.findByJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
member conn.findFirstByField<'TDoc> tableName field = member conn.findFirstByFields<'TDoc> tableName howMatched fields =
WithProps.Find.firstByField<'TDoc> tableName field (Sql.existingConnection conn) WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in
/// the document; returns None if not found
member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.firstByFieldsOrdered<'TDoc>
tableName howMatched queryFields orderFields (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found /// Retrieve the first document matching a JSON containment query (@>); returns None if not found
member conn.findFirstByContains<'TDoc> tableName (criteria: obj) = member conn.findFirstByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn) WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the
/// document; returns None if not found
member conn.findFirstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
member conn.findFirstByJsonPath<'TDoc> tableName jsonPath = member conn.findFirstByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the
/// document; returns None if not found
member conn.findFirstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn)
/// Update an entire document by its ID /// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
WithProps.Update.byId tableName docId document (Sql.existingConnection conn) WithProps.Update.byId tableName docId document (Sql.existingConnection conn)
@ -122,8 +156,8 @@ module Extensions =
WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn) WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
member conn.patchByField tableName field (patch: 'TPatch) = member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn)
/// Patch documents using a JSON containment query in the WHERE clause (@>) /// Patch documents using a JSON containment query in the WHERE clause (@>)
member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
@ -137,9 +171,9 @@ module Extensions =
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn) WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document /// Remove fields from documents via a comparison on JSON fields in the document
member conn.removeFieldsByField tableName field fieldNames = member conn.removeFieldsByFields tableName howMatched fields fieldNames =
WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a JSON containment query (@>) /// Remove fields from documents via a JSON containment query (@>)
member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames = member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames =
@ -153,9 +187,8 @@ module Extensions =
member conn.deleteById tableName (docId: 'TKey) = member conn.deleteById tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (Sql.existingConnection conn) WithProps.Delete.byId tableName docId (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =) member conn.deleteByFields tableName howMatched fields =
member conn.deleteByField tableName field = WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn)
WithProps.Delete.byField tableName field (Sql.existingConnection conn)
/// Delete documents by matching a JSON containment query (@>) /// Delete documents by matching a JSON containment query (@>)
member conn.deleteByContains tableName (criteria: 'TContains) = member conn.deleteByContains tableName (criteria: 'TContains) =
@ -225,8 +258,8 @@ type NpgsqlConnectionCSharpExtensions =
/// Count matching documents using a JSON field comparison query (->> =) /// Count matching documents using a JSON field comparison query (->> =)
[<Extension>] [<Extension>]
static member inline CountByField(conn, tableName, field) = static member inline CountByFields(conn, tableName, howMatched, fields) =
WithProps.Count.byField tableName field (Sql.existingConnection conn) WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Count matching documents using a JSON containment query (@>) /// Count matching documents using a JSON containment query (@>)
[<Extension>] [<Extension>]
@ -245,8 +278,8 @@ type NpgsqlConnectionCSharpExtensions =
/// Determine if documents exist using a JSON field comparison query (->> =) /// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>] [<Extension>]
static member inline ExistsByField(conn, tableName, field) = static member inline ExistsByFields(conn, tableName, howMatched, fields) =
WithProps.Exists.byField tableName field (Sql.existingConnection conn) WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Determine if documents exist using a JSON containment query (@>) /// Determine if documents exist using a JSON containment query (@>)
[<Extension>] [<Extension>]
@ -263,6 +296,11 @@ type NpgsqlConnectionCSharpExtensions =
static member inline FindAll<'TDoc>(conn, tableName) = static member inline FindAll<'TDoc>(conn, tableName) =
WithProps.Find.All<'TDoc>(tableName, Sql.existingConnection conn) WithProps.Find.All<'TDoc>(tableName, Sql.existingConnection conn)
/// Retrieve all documents in the given table ordered by the given fields in the document
[<Extension>]
static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) =
WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, Sql.existingConnection conn)
/// Retrieve a document by its ID; returns None if not found /// Retrieve a document by its ID; returns None if not found
[<Extension>] [<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) =
@ -270,34 +308,71 @@ type NpgsqlConnectionCSharpExtensions =
/// Retrieve documents matching a JSON field comparison query (->> =) /// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>] [<Extension>]
static member inline FindByField<'TDoc>(conn, tableName, field) = static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
WithProps.Find.ByField<'TDoc>(tableName, field, Sql.existingConnection conn) WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document
[<Extension>]
static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) =
WithProps.Find.ByFieldsOrdered<'TDoc>(
tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>) /// Retrieve documents matching a JSON containment query (@>)
[<Extension>] [<Extension>]
static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) = static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) =
WithProps.Find.ByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) WithProps.Find.ByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document
[<Extension>]
static member inline FindByContainsOrdered<'TDoc>(conn, tableName, criteria: obj, orderFields) =
WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn)
/// Retrieve documents matching a JSON Path match query (@?) /// Retrieve documents matching a JSON Path match query (@?)
[<Extension>] [<Extension>]
static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) = static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) =
WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document
[<Extension>] [<Extension>]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = static member inline FindByJsonPathOrdered<'TDoc>(conn, tableName, jsonPath, orderFields) =
WithProps.Find.FirstByField<'TDoc>(tableName, field, Sql.existingConnection conn) WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the
/// document; returns null if not found
[<Extension>]
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>(
conn, tableName, howMatched, queryFields, orderFields) =
WithProps.Find.FirstByFieldsOrdered<'TDoc>(
tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found /// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document;
/// returns None if not found
[<Extension>]
static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null>(
conn, tableName, criteria: obj, orderFields) =
WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document;
/// returns None if not found
[<Extension>]
static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null>(conn, tableName, jsonPath, orderFields) =
WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn)
/// Update an entire document by its ID /// Update an entire document by its ID
[<Extension>] [<Extension>]
static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) = static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) =
@ -315,8 +390,8 @@ type NpgsqlConnectionCSharpExtensions =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>] [<Extension>]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) =
WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn)
/// Patch documents using a JSON containment query in the WHERE clause (@>) /// Patch documents using a JSON containment query in the WHERE clause (@>)
[<Extension>] [<Extension>]
@ -331,22 +406,22 @@ type NpgsqlConnectionCSharpExtensions =
/// Remove fields from a document by the document's ID /// Remove fields from a document by the document's ID
[<Extension>] [<Extension>]
static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) = static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) =
WithProps.RemoveFields.ById(tableName, docId, fieldNames, Sql.existingConnection conn) WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document /// Remove fields from documents via a comparison on JSON fields in the document
[<Extension>] [<Extension>]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
WithProps.RemoveFields.ByField(tableName, field, fieldNames, Sql.existingConnection conn) WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a JSON containment query (@>) /// Remove fields from documents via a JSON containment query (@>)
[<Extension>] [<Extension>]
static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) = static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) =
WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, Sql.existingConnection conn) WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a JSON Path match query (@?) /// Remove fields from documents via a JSON Path match query (@?)
[<Extension>] [<Extension>]
static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) = static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) =
WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, Sql.existingConnection conn) WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (Sql.existingConnection conn)
/// Delete a document by its ID /// Delete a document by its ID
[<Extension>] [<Extension>]
@ -355,8 +430,8 @@ type NpgsqlConnectionCSharpExtensions =
/// Delete documents by matching a JSON field comparison query (->> =) /// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>] [<Extension>]
static member inline DeleteByField(conn, tableName, field) = static member inline DeleteByFields(conn, tableName, howMatched, fields) =
WithProps.Delete.byField tableName field (Sql.existingConnection conn) WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Delete documents by matching a JSON containment query (@>) /// Delete documents by matching a JSON containment query (@>)
[<Extension>] [<Extension>]

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ var customer = await Find.ById<string, Customer>("customer", "123");
// Find.byId type signature is string -> 'TKey -> Task<'TDoc option> // Find.byId type signature is string -> 'TKey -> Task<'TDoc option>
let! customer = Find.byId<string, Customer> "customer" "123" let! customer = Find.byId<string, Customer> "customer" "123"
``` ```
_(keys are treated as strings in the database)_ _(keys are treated as strings or numbers depending on their defintion; however, they are indexed as strings)_
Count customers in Atlanta (using JSON containment): Count customers in Atlanta (using JSON containment):

View File

@ -2,13 +2,13 @@
<PropertyGroup> <PropertyGroup>
<Description>Use SQLite as a document database</Description> <Description>Use SQLite as a document database</Description>
<PackageReleaseNotes>Overall v3 release; initial release supporting SQLite</PackageReleaseNotes>
<PackageTags>JSON Document SQLite</PackageTags> <PackageTags>JSON Document SQLite</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Library.fs" /> <Compile Include="Library.fs" />
<Compile Include="Extensions.fs" /> <Compile Include="Extensions.fs" />
<Compile Include="Compat.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" /> <None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>

269
src/Sqlite/Compat.fs Normal file
View File

@ -0,0 +1,269 @@
namespace BitBadger.Documents.Sqlite.Compat
open BitBadger.Documents
open BitBadger.Documents.Sqlite
[<AutoOpen>]
module Parameters =
/// Create a JSON field parameter
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams (F#) / AddFields (C#) instead ~ will be removed in v4.1">]
let addFieldParam name field parameters =
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FieldName">]
[<System.Obsolete "Use fieldNameParams (F#) / FieldNames (C#) instead ~ will be removed in v4.1">]
let fieldNameParam fieldNames =
fieldNameParams fieldNames
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
[<System.Obsolete "Use WhereByFields instead ~ will be removed in v4.1">]
let whereByField field paramName =
Query.whereByFields Any [ { field with ParameterName = Some paramName } ]
module WithConn =
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Count.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Exists.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field conn =
WithConn.Find.byFields<'TDoc> tableName Any [ field ] conn
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field, conn) =
WithConn.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field conn =
WithConn.Find.firstByFields<'TDoc> tableName Any [ field ] conn
/// 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>(tableName, field, conn) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) conn =
WithConn.Patch.byFields tableName Any [ field ] patch conn
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames conn =
WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Delete.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Count.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Exists.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field =
Find.byFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field) =
Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field =
Find.firstByFields<'TDoc> tableName Any [ field ]
/// 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>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) =
Patch.byFields tableName Any [ field ] patch
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames =
RemoveFields.byFields tableName Any [ field ] fieldNames
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Delete.byFields tableName Any [ field ]
open Microsoft.Data.Sqlite
/// F# Extensions for the NpgsqlConnection type
[<AutoOpen>]
module Extensions =
type SqliteConnection with
/// Count matching documents using a JSON field comparison query (->> =)
[<System.Obsolete "Use countByFields instead ~ will be removed in v4.1">]
member conn.countByField tableName field =
conn.countByFields tableName Any [ field ]
/// Determine if documents exist using a JSON field comparison query (->> =)
[<System.Obsolete "Use existsByFields instead ~ will be removed in v4.1">]
member conn.existsByField tableName field =
conn.existsByFields tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use findByFields instead ~ will be removed in v4.1">]
member conn.findByField<'TDoc> tableName field =
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<System.Obsolete "Use findFirstByFields instead ~ will be removed in v4.1">]
member conn.findFirstByField<'TDoc> tableName field =
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<System.Obsolete "Use patchByFields instead ~ will be removed in v4.1">]
member conn.patchByField tableName field (patch: 'TPatch) =
conn.patchByFields tableName Any [ field ] patch
/// Remove fields from documents via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead ~ will be removed in v4.1">]
member conn.removeFieldsByField tableName field fieldNames =
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// Delete documents by matching a JSON field comparison query (->> =)
[<System.Obsolete "Use deleteByFields instead ~ will be removed in v4.1">]
member conn.deleteByField tableName field =
conn.deleteByFields tableName Any [ field ]
open System.Runtime.CompilerServices
type SqliteConnectionCSharpCompatExtensions =
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use CountByFields instead ~ will be removed in v4.1">]
static member inline CountByField(conn, tableName, field) =
WithConn.Count.byFields tableName Any [ field ] conn
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead ~ will be removed in v4.1">]
static member inline ExistsByField(conn, tableName, field) =
WithConn.Exists.byFields tableName Any [ field ] conn
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use FindByFields instead ~ will be removed in v4.1">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithConn.Find.ByFields<'TDoc>(tableName, Any, [ field ], conn)
/// 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>(conn, tableName, field) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
[<System.Obsolete "Use PatchByFields instead ~ will be removed in v4.1">]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) =
WithConn.Patch.byFields tableName Any [ field ] patch conn
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead ~ will be removed in v4.1">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead ~ will be removed in v4.1">]
static member inline DeleteByField(conn, tableName, field) =
WithConn.Delete.byFields tableName Any [ field ] conn

View File

@ -1,5 +1,6 @@
namespace BitBadger.Documents.Sqlite namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
/// F# extensions for the SqliteConnection type /// F# extensions for the SqliteConnection type
@ -34,43 +35,56 @@ module Extensions =
/// Insert a new document /// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) = member conn.insert<'TDoc> tableName (document: 'TDoc) =
WithConn.insert<'TDoc> tableName document conn WithConn.Document.insert<'TDoc> tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
member conn.save<'TDoc> tableName (document: 'TDoc) = member conn.save<'TDoc> tableName (document: 'TDoc) =
WithConn.save tableName document conn WithConn.Document.save tableName document conn
/// Count all documents in a table /// Count all documents in a table
member conn.countAll tableName = member conn.countAll tableName =
WithConn.Count.all tableName conn WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field /// Count matching documents using a comparison on JSON fields
member conn.countByField tableName field = member conn.countByFields tableName howMatched fields =
WithConn.Count.byField tableName field conn WithConn.Count.byFields tableName howMatched fields conn
/// Determine if a document exists for the given ID /// Determine if a document exists for the given ID
member conn.existsById tableName (docId: 'TKey) = member conn.existsById tableName (docId: 'TKey) =
WithConn.Exists.byId tableName docId conn WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field /// Determine if a document exists using a comparison on JSON fields
member conn.existsByField tableName field = member conn.existsByFields tableName howMatched fields =
WithConn.Exists.byField tableName field conn WithConn.Exists.byFields tableName howMatched fields conn
/// Retrieve all documents in the given table /// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName = member conn.findAll<'TDoc> tableName =
WithConn.Find.all<'TDoc> tableName conn WithConn.Find.all<'TDoc> tableName conn
/// 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
/// Retrieve a document by its ID /// Retrieve a document by its ID
member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) = member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) =
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
member conn.findByField<'TDoc> tableName field = member conn.findByFields<'TDoc> tableName howMatched fields =
WithConn.Find.byField<'TDoc> tableName field conn WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
member conn.findFirstByField<'TDoc> tableName field = member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithConn.Find.firstByField<'TDoc> tableName field conn WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// 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
/// 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
/// Update an entire document by its ID /// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
@ -84,25 +98,25 @@ module Extensions =
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) = member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field /// Patch documents using a comparison on JSON fields
member conn.patchByField tableName field (patch: 'TPatch) = member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn WithConn.Patch.byFields tableName howMatched fields patch conn
/// Remove fields from a document by the document's ID /// Remove fields from a document by the document's ID
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
WithConn.RemoveFields.byId tableName docId fieldNames conn WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove a field from a document via a comparison on a JSON field in the document /// Remove a field from a document via a comparison on JSON fields in the document
member conn.removeFieldsByField tableName field fieldNames = member conn.removeFieldsByFields tableName howMatched fields fieldNames =
WithConn.RemoveFields.byField tableName field fieldNames conn WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Delete a document by its ID /// Delete a document by its ID
member conn.deleteById tableName (docId: 'TKey) = member conn.deleteById tableName (docId: 'TKey) =
WithConn.Delete.byId tableName docId conn WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field /// Delete documents by matching a comparison on JSON fields
member conn.deleteByField tableName field = member conn.deleteByFields tableName howMatched fields =
WithConn.Delete.byField tableName field conn WithConn.Delete.byFields tableName howMatched fields conn
open System.Runtime.CompilerServices open System.Runtime.CompilerServices
@ -145,52 +159,69 @@ type SqliteConnectionCSharpExtensions =
/// Insert a new document /// Insert a new document
[<Extension>] [<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.insert<'TDoc> tableName document conn WithConn.Document.insert<'TDoc> tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>] [<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) = static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.save<'TDoc> tableName document conn WithConn.Document.save<'TDoc> tableName document conn
/// Count all documents in a table /// Count all documents in a table
[<Extension>] [<Extension>]
static member inline CountAll(conn, tableName) = static member inline CountAll(conn, tableName) =
WithConn.Count.all tableName conn WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field /// Count matching documents using a comparison on JSON fields
[<Extension>] [<Extension>]
static member inline CountByField(conn, tableName, field) = static member inline CountByFields(conn, tableName, howMatched, fields) =
WithConn.Count.byField tableName field conn WithConn.Count.byFields tableName howMatched fields conn
/// Determine if a document exists for the given ID /// Determine if a document exists for the given ID
[<Extension>] [<Extension>]
static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) = static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Exists.byId tableName docId conn WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field /// Determine if a document exists using a comparison on JSON fields
[<Extension>] [<Extension>]
static member inline ExistsByField(conn, tableName, field) = static member inline ExistsByFields(conn, tableName, howMatched, fields) =
WithConn.Exists.byField tableName field conn WithConn.Exists.byFields tableName howMatched fields conn
/// Retrieve all documents in the given table /// Retrieve all documents in the given table
[<Extension>] [<Extension>]
static member inline FindAll<'TDoc>(conn, tableName) = static member inline FindAll<'TDoc>(conn, tableName) =
WithConn.Find.All<'TDoc>(tableName, conn) WithConn.Find.All<'TDoc>(tableName, conn)
/// 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)
/// Retrieve a document by its ID /// Retrieve a document by its ID
[<Extension>] [<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(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) WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
[<Extension>] [<Extension>]
static member inline FindByField<'TDoc>(conn, tableName, field) = static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
WithConn.Find.ByField<'TDoc>(tableName, field, conn) WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
[<Extension>] [<Extension>]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) =
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn) WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) =
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// 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>(
conn, tableName, howMatched, queryFields, orderFields) =
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// Update an entire document by its ID /// Update an entire document by its ID
[<Extension>] [<Extension>]
@ -207,27 +238,33 @@ type SqliteConnectionCSharpExtensions =
static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) = static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field /// Patch documents using a comparison on JSON fields
[<Extension>] [<Extension>]
static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) = static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn WithConn.Patch.byFields tableName howMatched fields patch conn
/// Remove fields from a document by the document's ID /// Remove fields from a document by the document's ID
[<Extension>] [<Extension>]
static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) = static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) =
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn) WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove fields from documents via a comparison on a JSON field in the document /// Remove fields from documents via a comparison on JSON fields in the document
[<Extension>] [<Extension>]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn) WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Delete a document by its ID /// Delete a document by its ID
[<Extension>] [<Extension>]
static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) = static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Delete.byId tableName docId conn WithConn.Delete.byId tableName docId conn
/// 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 /// Delete documents by matching a comparison on a JSON field
[<Extension>] [<Extension>]
[<System.Obsolete "Use DeleteByFields instead; will be removed in v4">]
static member inline DeleteByField(conn, tableName, field) = static member inline DeleteByField(conn, tableName, field) =
WithConn.Delete.byField tableName field conn conn.DeleteByFields(tableName, Any, [ field ])

View File

@ -31,20 +31,55 @@ module Configuration =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "WhereByField">] [<CompiledName "WhereByFields">]
let whereByField field paramName = let whereByFields (howMatched: FieldMatch) fields =
let theRest = let name = ParameterName()
match field.Op with fields
| EX | NEX -> "" |> Seq.map (fun it ->
| BT -> $" {paramName}min AND {paramName}max" match it.Comparison with
| _ -> $" %s{paramName}" | Exists | NotExists -> $"{it.Path SQLite AsSql} {it.Comparison.OpSql}"
$"data->>'{field.Name}' {field.Op}{theRest}" | Between _ ->
let p = name.Derive it.ParameterName
$"{it.Path SQLite AsSql} {it.Comparison.OpSql} {p}min AND {p}max"
| In values ->
let p = name.Derive it.ParameterName
let paramNames = values |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", "
$"{it.Path SQLite AsSql} {it.Comparison.OpSql} ({paramNames})"
| InArray (table, values) ->
let p = name.Derive it.ParameterName
let paramNames = values |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", "
$"EXISTS (SELECT 1 FROM json_each({table}.data, '$.{it.Name}') WHERE value IN ({paramNames}))"
| _ -> $"{it.Path SQLite AsSql} {it.Comparison.OpSql} {name.Derive it.ParameterName}")
|> String.concat $" {howMatched} "
/// Create a WHERE clause fragment to implement an ID-based query /// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">] [<CompiledName "WhereById">]
let whereById paramName = let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName whereByFields Any [ { Field.Equal (Configuration.idField ()) 0 with ParameterName = Some paramName } ]
/// Create an UPDATE statement to patch documents
[<CompiledName "Patch">]
let patch tableName =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data))"
/// Create an UPDATE statement to remove fields from documents
[<CompiledName "RemoveFields">]
let removeFields tableName (parameters: SqliteParameter seq) =
let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames})"
/// Create a query by a document's ID
[<CompiledName "ById">]
let byId<'TKey> statement (docId: 'TKey) =
Query.statementWhere
statement
(whereByFields Any [ { Field.Equal (Configuration.idField ()) docId with ParameterName = Some "@id" } ])
/// Create a query on JSON fields
[<CompiledName "ByFields">]
let byFields statement howMatched fields =
Query.statementWhere statement (whereByFields howMatched fields)
/// Data definition /// Data definition
module Definition = module Definition =
@ -54,106 +89,6 @@ module Query =
let ensureTable name = let ensureTable name =
Query.Definition.ensureTableFor name "TEXT" Query.Definition.ensureTableFor name "TEXT"
/// Query to update a document
[<CompiledName "Update">]
let update tableName =
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
/// Queries for counting documents
module Count =
/// Query to count all documents in a table
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Queries for determining document existence
module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Document patching (partial update) queries
module Patch =
/// Create an UPDATE statement to patch documents
let internal update tableName whereClause =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE %s{whereClause}"
/// Query to patch (partially update) a document by its ID
[<CompiledName "ById">]
let byId tableName =
whereById "@id" |> update tableName
/// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
whereByField field "@field" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
/// Create an UPDATE statement to remove parameters
let internal update tableName (parameters: SqliteParameter list) whereClause =
let paramNames = parameters |> List.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE {whereClause}"
/// Query to remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName parameters =
whereById "@id" |> update tableName parameters
/// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) =
byId tableName (List.ofSeq parameters)
/// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "FSharpByField">]
let byField tableName field parameters =
whereByField field "@field" |> update tableName parameters
/// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters)
/// Queries to delete documents
module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Parameter handling helpers /// Parameter handling helpers
[<AutoOpen>] [<AutoOpen>]
@ -169,39 +104,41 @@ module Parameters =
let jsonParam name (it: 'TJson) = let jsonParam name (it: 'TJson) =
SqliteParameter(name, Configuration.serializer().Serialize it) SqliteParameter(name, Configuration.serializer().Serialize it)
/// Create JSON field parameters
[<CompiledName "AddFields">]
let addFieldParams fields parameters =
let name = ParameterName()
fields
|> Seq.map (fun it ->
seq {
match it.Comparison with
| Exists | NotExists -> ()
| Between (min, max) ->
let p = name.Derive it.ParameterName
yield! [ SqliteParameter($"{p}min", min); SqliteParameter($"{p}max", max) ]
| In values | InArray (_, values) ->
let p = name.Derive it.ParameterName
yield! values |> Seq.mapi (fun idx v -> SqliteParameter($"{p}_{idx}", v))
| Equal v | Greater v | GreaterOrEqual v | Less v | LessOrEqual v | NotEqual v ->
yield SqliteParameter(name.Derive it.ParameterName, v) })
|> Seq.collect id
|> Seq.append parameters
|> Seq.toList
|> Seq.ofList
/// Create a JSON field parameter (name "@field") /// Create a JSON field parameter (name "@field")
[<CompiledName "FSharpAddField">] [<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams instead; will be removed in v4">]
let addFieldParam name field parameters = let addFieldParam name field parameters =
match field.Op with addFieldParams [ { field with ParameterName = Some name } ] parameters
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
SqliteParameter($"{name}min", values[0]) :: SqliteParameter($"{name}max", values[1]) :: parameters
| _ -> SqliteParameter(name, field.Value) :: parameters
/// Create a JSON field parameter (name "@field")
let AddField(name, field, parameters) =
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
// let min = SqliteParameter($"{name}min", SqliteType.Integer)
// min.Value <- values[0]
// let max = SqliteParameter($"{name}max", SqliteType.Integer)
// max.Value <- values[1]
[ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ]
// [min; max]
|> Seq.append parameters
| _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters /// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FSharpFieldNames">] [<CompiledName "FieldNames">]
let fieldNameParams paramName (fieldNames: string list) = let fieldNameParams paramName fieldNames =
fieldNames |> List.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}")) fieldNames
|> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.%s{name}"))
/// Append JSON field name parameters for the given field names to the given parameters |> Seq.toList
let FieldNames(paramName, fieldNames: string seq) = |> Seq.ofList
fieldNames |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}"))
/// An empty parameter sequence /// An empty parameter sequence
[<CompiledName "None">] [<CompiledName "None">]
@ -323,23 +260,42 @@ module WithConn =
[<CompiledName "EnsureTable">] [<CompiledName "EnsureTable">]
let ensureTable name conn = backgroundTask { let ensureTable name conn = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn do! Custom.nonQuery (Query.Definition.ensureKey name SQLite) [] conn
} }
/// Create an index on a document table /// Create an index on a document table
[<CompiledName "EnsureFieldIndex">] [<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields conn = let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn
/// Insert a new document /// Commands to add documents
[<CompiledName "Insert">] [<AutoOpen>]
let insert<'TDoc> tableName (document: 'TDoc) conn = module Document =
Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") /// Insert a new document
[<CompiledName "Save">] [<CompiledName "Insert">]
let save<'TDoc> tableName (document: 'TDoc) conn = let insert<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn 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}'), 0) + 1 FROM {tableName})"
| Guid -> $"'{AutoId.GenerateGuid()}'"
| RandomString -> $"'{AutoId.GenerateRandomString(Configuration.idStringLength ())}'"
| Disabled -> "@data"
|> function it -> $"json_set(@data, '$.{idField}', {it})"
else "@data"
(Query.insert tableName).Replace("@data", dataParam)
Custom.nonQuery query [ jsonParam "@data" document ] conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn
/// Commands to count documents /// Commands to count documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -348,12 +304,13 @@ module WithConn =
/// Count all documents in a table /// Count all documents in a table
[<CompiledName "All">] [<CompiledName "All">]
let all tableName conn = let all tableName conn =
Custom.scalar (Query.Count.all tableName) [] toCount conn Custom.scalar (Query.count tableName) [] toCount conn
/// Count matching documents using a comparison on a JSON field /// Count matching documents using a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field conn = let byFields tableName howMatched fields conn =
Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount conn Custom.scalar
(Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount conn
/// Commands to determine if documents exist /// Commands to determine if documents exist
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -362,12 +319,16 @@ module WithConn =
/// Determine if a document exists for the given ID /// Determine if a document exists for the given ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn = let byId tableName (docId: 'TKey) conn =
Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists conn Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists conn
/// Determine if a document exists using a comparison on a JSON field /// Determine if a document exists using a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field conn = let byFields tableName howMatched fields conn =
Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists conn Custom.scalar
(Query.exists tableName (Query.whereByFields howMatched fields))
(addFieldParams fields [])
toExists
conn
/// Commands to retrieve documents /// Commands to retrieve documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -376,42 +337,99 @@ module WithConn =
/// Retrieve all documents in the given table /// Retrieve all documents in the given table
[<CompiledName "FSharpAll">] [<CompiledName "FSharpAll">]
let all<'TDoc> tableName conn = let all<'TDoc> tableName conn =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> conn
/// Retrieve all documents in the given table /// Retrieve all documents in the given table
let All<'TDoc>(tableName, conn) = let All<'TDoc>(tableName, conn) =
Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn) Custom.List(Query.find tableName, [], fromData<'TDoc>, conn)
/// Retrieve all documents in the given table ordered by the given fields in the document
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields conn =
Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields SQLite) [] fromData<'TDoc> conn
/// Retrieve all documents in the given table ordered by the given fields in the document
let AllOrdered<'TDoc>(tableName, orderFields, conn) =
Custom.List(Query.find tableName + Query.orderBy orderFields SQLite, [], fromData<'TDoc>, conn)
/// Retrieve a document by its ID (returns None if not found) /// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">] [<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn = let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn =
Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn
/// Retrieve a document by its ID (returns null if not found) /// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) =
Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn) Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
[<CompiledName "FSharpByField">] [<CompiledName "FSharpByFields">]
let byField<'TDoc> tableName field conn = let byFields<'TDoc> tableName howMatched fields conn =
Custom.list<'TDoc> Custom.list<'TDoc>
(Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> conn (Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
let ByField<'TDoc>(tableName, field, conn) = let ByFields<'TDoc>(tableName, howMatched, fields, conn) =
Custom.List<'TDoc>( Custom.List<'TDoc>(
Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, conn) Query.byFields (Query.find tableName) howMatched fields,
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
[<CompiledName "FSharpFirstByField">] [<CompiledName "FSharpByFieldsOrdered">]
let firstByField<'TDoc> tableName field conn = let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite,
addFieldParams queryFields [],
fromData<'TDoc>,
conn)
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields conn =
Custom.single Custom.single
$"{Query.Find.byField tableName field} LIMIT 1" (addFieldParam "@field" field []) fromData<'TDoc> conn $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1"
(addFieldParams fields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) =
Custom.Single( Custom.Single(
$"{Query.Find.byField tableName field} LIMIT 1", addFieldParam "@field" field [], fromData<'TDoc>, conn) $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning
/// only the first result
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn =
Custom.single
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1"
(addFieldParams queryFields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning
/// only the first result
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, conn) =
Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1",
addFieldParams queryFields [],
fromData<'TDoc>,
conn)
/// Commands to update documents /// Commands to update documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -420,7 +438,10 @@ module WithConn =
/// Update an entire document by its ID /// Update an entire document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) conn = let byId tableName (docId: 'TKey) (document: 'TDoc) conn =
Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn Custom.nonQuery
(Query.statementWhere (Query.update tableName) (Query.whereById "@id"))
[ idParam docId; jsonParam "@data" document ]
conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document /// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">] [<CompiledName "FSharpByFunc">]
@ -438,38 +459,38 @@ module WithConn =
/// Patch a document by its ID /// Patch a document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) conn = let byId tableName (docId: 'TKey) (patch: 'TPatch) conn =
Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] conn
/// Patch documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) =
Custom.nonQuery Custom.nonQuery
(Query.Patch.byField tableName field) (addFieldParam "@field" field [ jsonParam "@data" patch ]) conn (Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] conn
/// Patch documents using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) conn =
Custom.nonQuery
(Query.byFields (Query.patch tableName) howMatched fields)
(addFieldParams fields [ jsonParam "@data" patch ])
conn
/// Commands to remove fields from documents /// Commands to remove fields from documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module RemoveFields = module RemoveFields =
/// Remove fields from a document by the document's ID /// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames conn = let byId tableName (docId: 'TKey) fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery (Query.RemoveFields.byId tableName nameParams) (idParam docId :: nameParams) conn Custom.nonQuery
(Query.byId (Query.removeFields tableName nameParams) docId)
(idParam docId |> Seq.singleton |> Seq.append nameParams)
conn
/// Remove fields from a document by the document's ID /// Remove fields from documents via a comparison on JSON fields in the document
let ById(tableName, docId: 'TKey, fieldNames, conn) = [<CompiledName "ByFields">]
byId tableName docId (List.ofSeq fieldNames) conn let byFields tableName howMatched fields fieldNames conn =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery Custom.nonQuery
(Query.RemoveFields.byField tableName field nameParams) (addFieldParam "@field" field nameParams) conn (Query.byFields (Query.removeFields tableName nameParams) howMatched fields)
(addFieldParams fields nameParams)
/// Remove fields from documents via a comparison on a JSON field in the document conn
let ByField(tableName, field, fieldNames, conn) =
byField tableName field (List.ofSeq fieldNames) conn
/// Commands to delete documents /// Commands to delete documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -478,12 +499,12 @@ module WithConn =
/// Delete a document by its ID /// Delete a document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn = let byId tableName (docId: 'TKey) conn =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] conn
/// Delete documents by matching a comparison on a JSON field /// Delete documents by matching a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field conn = let byFields tableName howMatched fields conn =
Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) conn Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn
/// Commands to execute custom SQL queries /// Commands to execute custom SQL queries
@ -529,6 +550,7 @@ module Custom =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// Functions to create tables and indexes /// Functions to create tables and indexes
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Definition = module Definition =
@ -545,6 +567,7 @@ module Definition =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Definition.ensureFieldIndex tableName indexName fields conn WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// Document insert/save functions /// Document insert/save functions
[<AutoOpen>] [<AutoOpen>]
module Document = module Document =
@ -553,13 +576,14 @@ module Document =
[<CompiledName "Insert">] [<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) = let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.insert tableName document conn WithConn.Document.insert tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">] [<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) = let save<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.save tableName document conn WithConn.Document.save tableName document conn
/// Commands to count documents /// Commands to count documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -571,11 +595,12 @@ module Count =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Count.all tableName conn WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field /// Count matching documents using a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field = let byFields tableName howMatched fields =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Count.byField tableName field conn WithConn.Count.byFields tableName howMatched fields conn
/// Commands to determine if documents exist /// Commands to determine if documents exist
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -587,11 +612,12 @@ module Exists =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Exists.byId tableName docId conn WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field /// Determine if a document exists using a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field = let byFields tableName howMatched fields =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Exists.byField tableName field conn WithConn.Exists.byFields tableName howMatched fields conn
/// Commands to determine if documents exist /// Commands to determine if documents exist
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -608,6 +634,17 @@ module Find =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.All<'TDoc>(tableName, conn) WithConn.Find.All<'TDoc>(tableName, conn)
/// Retrieve all documents in the given table ordered by the given fields in the document
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.allOrdered<'TDoc> tableName orderFields conn
/// Retrieve all documents in the given table ordered by the given fields in the document
let AllOrdered<'TDoc> tableName orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn)
/// Retrieve a document by its ID (returns None if not found) /// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">] [<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId = let byId<'TKey, 'TDoc> tableName docId =
@ -619,27 +656,52 @@ module Find =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
[<CompiledName "FSharpByField">] [<CompiledName "FSharpByFields">]
let byField<'TDoc> tableName field = let byFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.byField<'TDoc> tableName field conn WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field /// Retrieve documents via a comparison on JSON fields
let ByField<'TDoc>(tableName, field) = let ByFields<'TDoc>(tableName, howMatched, fields) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.ByField<'TDoc>(tableName, field, conn) WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
[<CompiledName "FSharpFirstByField">] [<CompiledName "FSharpByFieldsOrdered">]
let firstByField<'TDoc> tableName field = let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.firstByField<'TDoc> tableName field conn WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn) WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only
/// the first result
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only
/// the first result
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// Commands to update documents /// Commands to update documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -662,6 +724,7 @@ module Update =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Update.ByFunc(tableName, idFunc, document, conn) WithConn.Update.ByFunc(tableName, idFunc, document, conn)
/// Commands to patch (partially update) documents /// Commands to patch (partially update) documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Patch = module Patch =
@ -672,37 +735,29 @@ module Patch =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Patch.byId tableName docId patch conn WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field in the WHERE clause /// Patch documents using a comparison on JSON fields in the WHERE clause
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field (patch: 'TPatch) = let byFields tableName howMatched fields (patch: 'TPatch) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Patch.byField tableName field patch conn WithConn.Patch.byFields tableName howMatched fields patch conn
/// Commands to remove fields from documents /// Commands to remove fields from documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module RemoveFields = module RemoveFields =
/// Remove fields from a document by the document's ID /// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">] [<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames = let byId tableName (docId: 'TKey) fieldNames =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.RemoveFields.byId tableName docId fieldNames conn WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove fields from a document by the document's ID /// Remove field from documents via a comparison on JSON fields in the document
let ById(tableName, docId: 'TKey, fieldNames) = [<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn) WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Remove field from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byField tableName field fieldNames conn
/// Remove field from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames) =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn)
/// Commands to delete documents /// Commands to delete documents
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -714,8 +769,8 @@ module Delete =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Delete.byId tableName docId conn WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field /// Delete documents by matching a comparison on JSON fields
[<CompiledName "ByField">] [<CompiledName "ByFields">]
let byField tableName field = let byFields tableName howMatched fields =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Delete.byField tableName field conn WithConn.Delete.byFields tableName howMatched fields conn

View File

@ -73,13 +73,13 @@ Count customers in Atlanta:
```csharp ```csharp
// C#; parameters are table name, field, operator, and value // C#; parameters are table name, field, operator, and value
// Count.ByField type signature is Func<string, Field, Task<long>> // Count.ByField type signature is Func<string, Field, Task<long>>
var customerCount = await Count.ByField("customer", Field.EQ("City", "Atlanta")); var customerCount = await Count.ByField("customer", Field.Equal("City", "Atlanta"));
``` ```
```fsharp ```fsharp
// F# // F#
// Count.byField type signature is string -> Field -> Task<int64> // Count.byField type signature is string -> Field -> Task<int64>
let! customerCount = Count.byField "customer" (Field.EQ "City" "Atlanta") let! customerCount = Count.byField "customer" (Field.Equal "City" "Atlanta")
``` ```
Delete customers in Chicago: _(no offense, Second City; just an example...)_ Delete customers in Chicago: _(no offense, Second City; just an example...)_
@ -87,13 +87,13 @@ Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp ```csharp
// C#; parameters are same as above, except return is void // C#; parameters are same as above, except return is void
// Delete.ByField type signature is Func<string, Field, Task> // Delete.ByField type signature is Func<string, Field, Task>
await Delete.ByField("customer", Field.EQ("City", "Chicago")); await Delete.ByField("customer", Field.Equal("City", "Chicago"));
``` ```
```fsharp ```fsharp
// F# // F#
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit> // Delete.byField type signature is string -> string -> Op -> obj -> Task<unit>
do! Delete.byField "customer" (Field.EQ "City" "Chicago") do! Delete.byField "customer" (Field.Equal "City" "Chicago")
``` ```
## More Information ## More Information

View File

@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,6 @@
using Expecto.CSharp; using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.FSharp.Collections; using Microsoft.FSharp.Core;
namespace BitBadger.Documents.Tests.CSharp; namespace BitBadger.Documents.Tests.CSharp;
@ -21,209 +21,645 @@ internal class TestSerializer : IDocumentSerializer
public static class CommonCSharpTests public static class CommonCSharpTests
{ {
/// <summary> /// <summary>
/// Unit tests /// Unit tests for the OpSql property of the Comparison discriminated union
/// </summary> /// </summary>
[Tests] private static readonly Test OpTests = TestList("Comparison.OpSql",
public static readonly Test Unit = TestList("Common.C# Unit", new[] [
{ TestCase("Equal succeeds", () =>
TestSequenced( {
TestList("Configuration", new[] Expect.equal(Comparison.NewEqual("").OpSql, "=", "The Equals SQL was not correct");
}),
TestCase("Greater succeeds", () =>
{
Expect.equal(Comparison.NewGreater("").OpSql, ">", "The Greater SQL was not correct");
}),
TestCase("GreaterOrEqual succeeds", () =>
{
Expect.equal(Comparison.NewGreaterOrEqual("").OpSql, ">=", "The GreaterOrEqual SQL was not correct");
}),
TestCase("Less succeeds", () =>
{
Expect.equal(Comparison.NewLess("").OpSql, "<", "The Less SQL was not correct");
}),
TestCase("LessOrEqual succeeds", () =>
{
Expect.equal(Comparison.NewLessOrEqual("").OpSql, "<=", "The LessOrEqual SQL was not correct");
}),
TestCase("NotEqual succeeds", () =>
{
Expect.equal(Comparison.NewNotEqual("").OpSql, "<>", "The NotEqual SQL was not correct");
}),
TestCase("Between succeeds", () =>
{
Expect.equal(Comparison.NewBetween("", "").OpSql, "BETWEEN", "The Between SQL was not correct");
}),
TestCase("In succeeds", () =>
{
Expect.equal(Comparison.NewIn([]).OpSql, "IN", "The In SQL was not correct");
}),
TestCase("InArray succeeds", () =>
{
Expect.equal(Comparison.NewInArray("", []).OpSql, "?|", "The InArray SQL was not correct");
}),
TestCase("Exists succeeds", () =>
{
Expect.equal(Comparison.Exists.OpSql, "IS NOT NULL", "The Exists SQL was not correct");
}),
TestCase("NotExists succeeds", () =>
{
Expect.equal(Comparison.NotExists.OpSql, "IS NULL", "The NotExists SQL was not correct");
})
]);
/// <summary>
/// Unit tests for the Field class
/// </summary>
private static readonly Test FieldTests = TestList("Field",
[
TestCase("Equal succeeds", () =>
{
var field = Field.Equal("Test", 14);
Expect.equal(field.Name, "Test", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewEqual(14), "Comparison incorrect");
}),
TestCase("Greater succeeds", () =>
{
var field = Field.Greater("Great", "night");
Expect.equal(field.Name, "Great", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewGreater("night"), "Comparison incorrect");
}),
TestCase("GreaterOrEqual succeeds", () =>
{
var field = Field.GreaterOrEqual("Nice", 88L);
Expect.equal(field.Name, "Nice", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewGreaterOrEqual(88L), "Comparison incorrect");
}),
TestCase("Less succeeds", () =>
{
var field = Field.Less("Lesser", "seven");
Expect.equal(field.Name, "Lesser", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewLess("seven"), "Comparison incorrect");
}),
TestCase("LessOrEqual succeeds", () =>
{
var field = Field.LessOrEqual("Nobody", "KNOWS");
Expect.equal(field.Name, "Nobody", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewLessOrEqual("KNOWS"), "Comparison incorrect");
}),
TestCase("NotEqual succeeds", () =>
{
var field = Field.NotEqual("Park", "here");
Expect.equal(field.Name, "Park", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewNotEqual("here"), "Comparison incorrect");
}),
TestCase("Between succeeds", () =>
{
var field = Field.Between("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewBetween(18, 49), "Comparison incorrect");
}),
TestCase("In succeeds", () =>
{
var field = Field.In("Here", [8, 16, 32]);
Expect.equal(field.Name, "Here", "Field name incorrect");
Expect.isTrue(field.Comparison.IsIn, "Comparison incorrect");
Expect.sequenceEqual(((Comparison.In)field.Comparison).Values, [8, 16, 32], "Value incorrect");
}),
TestCase("InArray succeeds", () =>
{
var field = Field.InArray("ArrayField", "table", ["x", "y", "z"]);
Expect.equal(field.Name, "ArrayField", "Field name incorrect");
Expect.isTrue(field.Comparison.IsInArray, "Comparison incorrect");
var it = (Comparison.InArray)field.Comparison;
Expect.equal(it.Table, "table", "Table name incorrect");
Expect.sequenceEqual(it.Values, ["x", "y", "z"], "Value incorrect");
}),
TestCase("Exists succeeds", () =>
{
var field = Field.Exists("Groovy");
Expect.equal(field.Name, "Groovy", "Field name incorrect");
Expect.isTrue(field.Comparison.IsExists, "Comparison incorrect");
}),
TestCase("NotExists succeeds", () =>
{
var field = Field.NotExists("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.isTrue(field.Comparison.IsNotExists, "Comparison incorrect");
}),
TestList("NameToPath",
[
TestCase("succeeds for PostgreSQL and a simple name", () =>
{ {
TestCase("UseSerializer succeeds", () => Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL, FieldFormat.AsSql),
{ "Path not constructed correctly");
try }),
{ TestCase("succeeds for SQLite and a simple name", () =>
Configuration.UseSerializer(new TestSerializer()); {
Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite, FieldFormat.AsSql),
"Path not constructed correctly");
}),
TestCase("succeeds for PostgreSQL and a nested name", () =>
{
Expect.equal("data#>>'{A,Long,Path,to,the,Property}'",
Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL, FieldFormat.AsSql),
"Path not constructed correctly");
}),
TestCase("succeeds for SQLite and a nested name", () =>
{
Expect.equal("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite, FieldFormat.AsSql),
"Path not constructed correctly");
})
]),
TestCase("WithParameterName succeeds", () =>
{
var 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");
}),
TestCase("WithQualifier succeeds", () =>
{
var 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",
[
TestCase("succeeds for a PostgreSQL single field with no qualifier", () =>
{
var field = Field.GreaterOrEqual("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL single field with a qualifier", () =>
{
var field = Field.Less("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with no qualifier", () =>
{
var field = Field.Equal("My.Nested.Field", "howdy");
Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with a qualifier", () =>
{
var field = Field.Equal("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a SQLite single field with no qualifier", () =>
{
var field = Field.GreaterOrEqual("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite single field with a qualifier", () =>
{
var field = Field.Less("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with no qualifier", () =>
{
var field = Field.Equal("My.Nested.Field", "howdy");
Expect.equal("data->'My'->'Nested'->>'Field'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with a qualifier", () =>
{
var field = Field.Equal("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data->'Nest'->>'Away'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
})
])
]);
var serialized = Configuration.Serializer().Serialize(new SubDocument /// <summary>
{ /// Unit tests for the FieldMatch enum
Foo = "howdy", /// </summary>
Bar = "bye" private static readonly Test FieldMatchTests = TestList("FieldMatch.ToString",
}); [
Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); TestCase("succeeds for Any", () =>
{
Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect");
}),
TestCase("succeeds for All", () =>
{
Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect");
})
]);
var deserialized = Configuration.Serializer() /// <summary>
.Deserialize<object>("{\"Something\":\"here\"}"); /// Unit tests for the ParameterName class
Expect.isNull(deserialized, "Specified serializer should have returned null"); /// </summary>
} private static readonly Test ParameterNameTests = TestList("ParameterName.Derive",
finally [
{ TestCase("succeeds with existing name", () =>
Configuration.UseSerializer(DocumentSerializer.Default); {
} ParameterName name = new();
}), Expect.equal(name.Derive(FSharpOption<string>.Some("@taco")), "@taco", "Name should have been @taco");
TestCase("Serializer returns configured serializer", () => Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Counter should not have advanced for named field");
}),
TestCase("Derive succeeds with non-existent name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Anonymous field name should have been returned");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field1",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field2",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field3",
"Counter should have advanced from previous call");
})
]);
/// <summary>
/// Unit tests for the AutoId enum
/// </summary>
private static readonly Test AutoIdTests = TestList("AutoId",
[
TestCase("GenerateGuid succeeds", () =>
{
var 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");
}),
TestCase("GenerateRandomString succeeds", () =>
{
foreach (var length in (int[]) [6, 8, 12, 20, 32, 57, 64])
{
var 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",
[
TestCase("succeeds when no auto ID is configured", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Disabled, new object(), "id"),
"Disabled auto-ID never needs an automatic ID");
}),
TestCase("fails for any when the ID property is not found", () =>
{
try
{ {
Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), _ = AutoId.NeedsAutoId(AutoId.Number, new { Key = "" }, "Id");
"Serializer should have been the same"); Expect.isTrue(false, "Non-existent ID property should have thrown an exception");
}), }
TestCase("UseIdField / IdField succeeds", () => catch (InvalidOperationException)
{ {
try // pass
{ }
Expect.equal(Configuration.IdField(), "Id", }),
"The default configured ID field was incorrect"); TestCase("succeeds for byte when the ID is zero", () =>
Configuration.UseIdField("id"); {
Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)0 }, "Id"),
} "Zero ID should have returned true");
finally }),
{ TestCase("succeeds for byte when the ID is non-zero", () =>
Configuration.UseIdField("Id"); {
} Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)4 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for short when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)0 }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for short when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)7 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for int when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0 }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for int when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 32 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for long when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0L }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for long when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 80L }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("fails for number when the ID is not a number", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.Number, new { Id = "" }, "Id");
Expect.isTrue(false, "Numeric ID against a string should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
}),
TestCase("succeeds for GUID when the ID is blank", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "" }, "Id"),
"Blank ID should have returned true");
}),
TestCase("succeeds for GUID when the ID is filled", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "abc" }, "Id"),
"Filled ID should have returned false");
}),
TestCase("fails for GUID when the ID is not a string", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.Guid, new { Id = 8 }, "Id");
Expect.isTrue(false, "String ID against a number should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
}),
TestCase("succeeds for RandomString when the ID is blank", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "" }, "Id"),
"Blank ID should have returned true");
}),
TestCase("succeeds for RandomString when the ID is filled", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "x" }, "Id"),
"Filled ID should have returned false");
}),
TestCase("fails for RandomString when the ID is not a string", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.RandomString, new { Id = 33 }, "Id");
Expect.isTrue(false, "String ID against a number should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
})
])
]);
/// <summary>
/// Unit tests for the Configuration static class
/// </summary>
private static readonly Test ConfigurationTests = TestList("Configuration",
[
TestCase("UseSerializer succeeds", () =>
{
try
{
Configuration.UseSerializer(new TestSerializer());
var serialized = Configuration.Serializer().Serialize(new SubDocument
{
Foo = "howdy",
Bar = "bye"
});
Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used");
var deserialized = Configuration.Serializer()
.Deserialize<object>("{\"Something\":\"here\"}");
Expect.isNull(deserialized, "Specified serializer should have returned null");
}
finally
{
Configuration.UseSerializer(DocumentSerializer.Default);
}
}),
TestCase("Serializer returns configured serializer", () =>
{
Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()),
"Serializer should have been the same");
}),
TestCase("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");
}
}),
TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () =>
{
try
{
Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled,
"The default auto-ID strategy was incorrect");
Configuration.UseAutoIdStrategy(AutoId.Guid);
Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid,
"The auto-ID strategy was not set correctly");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
}
}),
TestCase("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);
}
})
]);
/// <summary>
/// Unit tests for the Query static class
/// </summary>
private static readonly Test QueryTests = TestList("Query",
[
TestCase("StatementWhere succeeds", () =>
{
Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly");
}),
TestList("Definition",
[
TestCase("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",
[
TestCase("succeeds when a schema is present", () =>
{
Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))",
"CREATE INDEX for key statement with schema not constructed correctly");
}),
TestCase("succeeds when a schema is not present", () =>
{
Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))",
"CREATE INDEX for key statement without schema not constructed correctly");
}) })
})), ]),
TestList("Op", new[] TestList("EnsureIndexOn",
{ [
TestCase("EQ succeeds", () => TestCase("succeeds for multiple fields and directions", () =>
{
Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct");
}),
TestCase("GT succeeds", () =>
{
Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct");
}),
TestCase("GE succeeds", () =>
{
Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct");
}),
TestCase("LT succeeds", () =>
{
Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct");
}),
TestCase("LE succeeds", () =>
{
Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct");
}),
TestCase("NE succeeds", () =>
{
Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct");
}),
TestCase("BT succeeds", () =>
{
Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct");
}),
TestCase("EX succeeds", () =>
{
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct");
}),
TestCase("NEX succeeds", () =>
{
Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct");
})
}),
TestList("Field", new[]
{
TestCase("EQ succeeds", () =>
{
var field = Field.EQ("Test", 14);
Expect.equal(field.Name, "Test", "Field name incorrect");
Expect.equal(field.Op, Op.EQ, "Operator incorrect");
Expect.equal(field.Value, 14, "Value incorrect");
}),
TestCase("GT succeeds", () =>
{
var field = Field.GT("Great", "night");
Expect.equal(field.Name, "Great", "Field name incorrect");
Expect.equal(field.Op, Op.GT, "Operator incorrect");
Expect.equal(field.Value, "night", "Value incorrect");
}),
TestCase("GE succeeds", () =>
{
var field = Field.GE("Nice", 88L);
Expect.equal(field.Name, "Nice", "Field name incorrect");
Expect.equal(field.Op, Op.GE, "Operator incorrect");
Expect.equal(field.Value, 88L, "Value incorrect");
}),
TestCase("LT succeeds", () =>
{
var field = Field.LT("Lesser", "seven");
Expect.equal(field.Name, "Lesser", "Field name incorrect");
Expect.equal(field.Op, Op.LT, "Operator incorrect");
Expect.equal(field.Value, "seven", "Value incorrect");
}),
TestCase("LE succeeds", () =>
{
var field = Field.LE("Nobody", "KNOWS");
Expect.equal(field.Name, "Nobody", "Field name incorrect");
Expect.equal(field.Op, Op.LE, "Operator incorrect");
Expect.equal(field.Value, "KNOWS", "Value incorrect");
}),
TestCase("NE succeeds", () =>
{
var field = Field.NE("Park", "here");
Expect.equal(field.Name, "Park", "Field name incorrect");
Expect.equal(field.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value incorrect");
}),
TestCase("BT succeeds", () =>
{
var field = Field.BT("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Op, Op.BT, "Operator incorrect");
Expect.equal(((FSharpList<object>)field.Value).ToArray(), new object[] { 18, 49 }, "Value incorrect");
}),
TestCase("EX succeeds", () =>
{
var field = Field.EX("Groovy");
Expect.equal(field.Name, "Groovy", "Field name incorrect");
Expect.equal(field.Op, Op.EX, "Operator incorrect");
}),
TestCase("NEX succeeds", () =>
{
var field = Field.NEX("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.equal(field.Op, Op.NEX, "Operator incorrect");
})
}),
TestList("Query", new[]
{
TestCase("SelectFromTable succeeds", () =>
{
Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table",
"SELECT statement not correct");
}),
TestList("Definition", new[]
{
TestCase("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", new[]
{
TestCase("succeeds when a schema is present", () =>
{
Expect.equal(Query.Definition.EnsureKey("test.table"),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))",
"CREATE INDEX for key statement with schema not constructed correctly");
}),
TestCase("succeeds when a schema is not present", () =>
{
Expect.equal(Query.Definition.EnsureKey("table"),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))",
"CREATE INDEX for key statement without schema not constructed correctly");
})
}),
TestCase("EnsureIndexOn succeeds for multiple fields and directions", () =>
{ {
Expect.equal( Expect.equal(
Query.Definition.EnsureIndexOn("test.table", "gibberish", Query.Definition.EnsureIndexOn("test.table", "gibberish",
new[] { "taco", "guac DESC", "salsa ASC" }), ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite),
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
+ "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
"CREATE INDEX for multiple field statement incorrect"); "CREATE INDEX for multiple field statement incorrect");
}),
TestCase("succeeds for nested PostgreSQL field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))",
"CREATE INDEX for nested PostgreSQL field incorrect");
}),
TestCase("succeeds for nested SQLite field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->'a'->'b'->>'c'))",
"CREATE INDEX for nested SQLite field incorrect");
}) })
}), ])
TestCase("Insert succeeds", () => ]),
TestCase("Insert succeeds", () =>
{
Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct");
}),
TestCase("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");
}),
TestCase("Count succeeds", () =>
{
Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct");
}),
TestCase("Exists succeeds", () =>
{
Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it",
"Exists query not correct");
}),
TestCase("Find succeeds", () =>
{
Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct");
}),
TestCase("Update succeeds", () =>
{
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct");
}),
TestCase("Delete succeeds", () =>
{
Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct");
}),
TestList("OrderBy",
[
TestCase("succeeds for no fields", () =>
{ {
Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)");
Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)");
}), }),
TestCase("Save succeeds", () => TestCase("succeeds for PostgreSQL with one field and no direction", () =>
{ {
Expect.equal(Query.Save("tbl"), Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL),
"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", " ORDER BY data->>'TestField'", "Order By not constructed correctly");
"INSERT ON CONFLICT UPDATE statement not correct"); }),
TestCase("succeeds for SQLite with one field and no direction", () =>
{
Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite),
" ORDER BY data->>'TestField'", "Order By not constructed correctly");
}),
TestCase("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")
], Dialect.PostgreSQL),
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
TestCase("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")
], Dialect.SQLite),
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
TestCase("succeeds for PostgreSQL numeric fields", () =>
{
Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL),
" ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field");
}),
TestCase("succeeds for SQLite numeric fields", () =>
{
Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'",
"Order By not constructed correctly for numeric field");
}),
TestCase("succeeds for PostgreSQL case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field DESC NULLS FIRST")], Dialect.PostgreSQL),
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
"Order By not constructed correctly for case-insensitive field");
}),
TestCase("succeeds for SQLite case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field ASC NULLS LAST")], Dialect.SQLite),
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
"Order By not constructed correctly for case-insensitive field");
}) })
}) ])
}); ]);
/// <summary>
/// Unit tests
/// </summary>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit",
[
OpTests,
FieldTests,
FieldMatchTests,
ParameterNameTests,
AutoIdTests,
QueryTests,
TestSequenced(ConfigurationTests)
]);
} }

View File

@ -31,17 +31,17 @@ public class PostgresCSharpExtensionTests
/// Integration tests for the SQLite extension methods /// Integration tests for the SQLite extension methods
/// </summary> /// </summary>
[Tests] [Tests]
public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[] public static readonly Test Integration = TestList("Postgres.C#.Extensions",
{ [
TestList("CustomList", new[] TestList("CustomList",
{ [
TestCase("succeeds when data is found", async () => TestCase("succeeds when data is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var docs = await conn.CustomList(Query.SelectFromTable(PostgresDb.TableName), Parameters.None, var docs = await conn.CustomList(Query.Find(PostgresDb.TableName), Parameters.None,
Results.FromData<JsonDocument>); Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}), }),
@ -53,13 +53,13 @@ public class PostgresCSharpExtensionTests
var docs = await conn.CustomList( var docs = await conn.CustomList(
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }, [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))],
Results.FromData<JsonDocument>); Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("CustomSingle", new[] TestList("CustomSingle",
{ [
TestCase("succeeds when a row is found", async () => TestCase("succeeds when a row is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -67,7 +67,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData<JsonDocument>); [Tuple.Create("@id", Sql.@string("one"))], Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "one", "The incorrect document was returned"); Expect.equal(doc.Id, "one", "The incorrect document was returned");
}), }),
@ -78,12 +78,12 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>); [Tuple.Create("@id", Sql.@string("eighty"))], Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("CustomNonQuery", new[] TestList("CustomNonQuery",
{ [
TestCase("succeeds when operating on data", async () => TestCase("succeeds when operating on data", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -102,12 +102,12 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }); [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))]);
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); Expect.equal(remaining, 5, "There should be 5 documents remaining in the table");
}) })
}), ]),
TestCase("Scalar succeeds", async () => TestCase("Scalar succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -119,58 +119,64 @@ public class PostgresCSharpExtensionTests
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
var tableExists = () => conn.CustomScalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None,
Results.ToExists);
var keyExists = () => conn.CustomScalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None,
Results.ToExists);
var exists = await tableExists(); var exists = await TableExists();
var alsoExists = await keyExists(); var alsoExists = await KeyExists();
Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already");
await conn.EnsureTable("ensured"); await conn.EnsureTable("ensured");
exists = await tableExists(); exists = await TableExists();
alsoExists = await keyExists(); alsoExists = await KeyExists();
Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist"); Expect.isTrue(alsoExists, "The key index should now exist");
return;
Task<bool> KeyExists() =>
conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it",
Parameters.None, Results.ToExists);
Task<bool> TableExists() =>
conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it",
Parameters.None, Results.ToExists);
}), }),
TestCase("EnsureDocumentIndex succeeds", async () => TestCase("EnsureDocumentIndex succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
var indexExists = () => conn.CustomScalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", Parameters.None,
Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured"); await conn.EnsureTable("ensured");
await conn.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); await conn.EnsureDocumentIndex("ensured", DocumentIndex.Optimized);
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() =>
conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it",
Parameters.None, Results.ToExists);
}), }),
TestCase("EnsureFieldIndex succeeds", async () => TestCase("EnsureFieldIndex succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
var indexExists = () => conn.CustomScalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None,
Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured"); await conn.EnsureTable("ensured");
await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]);
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() =>
conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
}), }),
TestList("Insert", new[] TestList("Insert",
{ [
TestCase("succeeds", async () => TestCase("succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -198,9 +204,9 @@ public class PostgresCSharpExtensionTests
// This is what should have happened // This is what should have happened
} }
}) })
}), ]),
TestList("save", new[] TestList("Save",
{ [
TestCase("succeeds when a document is inserted", async () => TestCase("succeeds when a document is inserted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -230,7 +236,7 @@ public class PostgresCSharpExtensionTests
Expect.isNotNull(after, "There should have been a document returned post-update"); Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct");
}) })
}), ]),
TestCase("CountAll succeeds", async () => TestCase("CountAll succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -240,13 +246,14 @@ public class PostgresCSharpExtensionTests
var theCount = await conn.CountAll(PostgresDb.TableName); var theCount = await conn.CountAll(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents"); Expect.equal(theCount, 5, "There should have been 5 matching documents");
}), }),
TestCase("CountByField succeeds", async () => TestCase("CountByFields succeeds", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var theCount = await conn.CountByField(PostgresDb.TableName, Field.EQ("Value", "purple")); var theCount = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
Expect.equal(theCount, 2, "There should have been 2 matching documents"); Expect.equal(theCount, 2, "There should have been 2 matching documents");
}), }),
TestCase("CountByContains succeeds", async () => TestCase("CountByContains succeeds", async () =>
@ -267,8 +274,8 @@ public class PostgresCSharpExtensionTests
var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents"); Expect.equal(theCount, 3, "There should have been 3 matching documents");
}), }),
TestList("ExistsById", new[] TestList("ExistsById",
{ [
TestCase("succeeds when a document exists", async () => TestCase("succeeds when a document exists", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -287,16 +294,16 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsById(PostgresDb.TableName, "seven"); var exists = await conn.ExistsById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document"); Expect.isFalse(exists, "There should not have been an existing document");
}) })
}), ]),
TestList("ExistsByField", new[] TestList("ExistsByField",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EX("Sub")); var exists = await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Exists("Sub")]);
Expect.isTrue(exists, "There should have been existing documents"); Expect.isTrue(exists, "There should have been existing documents");
}), }),
TestCase("succeeds when documents do not exist", async () => TestCase("succeeds when documents do not exist", async () =>
@ -305,12 +312,13 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); var exists =
await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "six")]);
Expect.isFalse(exists, "There should not have been existing documents"); Expect.isFalse(exists, "There should not have been existing documents");
}) })
}), ]),
TestList("ExistsByContains", new[] TestList("ExistsByContains",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -329,9 +337,9 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" }); var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}), ]),
TestList("ExistsByJsonPath", new[] TestList("ExistsByJsonPath",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -350,9 +358,9 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}), ]),
TestList("FindAll", new[] TestList("FindAll",
{ [
TestCase("succeeds when there is data", async () => TestCase("succeeds when there is data", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -372,9 +380,47 @@ public class PostgresCSharpExtensionTests
var results = await conn.FindAll<JsonDocument>(PostgresDb.TableName); var results = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned"); Expect.isEmpty(results, "There should have been no documents returned");
}) })
}), ]),
TestList("FindById", new[] TestList("FindAllOrdered",
{ [
TestCase("succeeds when ordering numerically", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var results =
await conn.FindAllOrdered<JsonDocument>(PostgresDb.TableName, [Field.Named("n:NumValue")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five",
"The documents were not ordered correctly");
}),
TestCase("succeeds when ordering numerically descending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var results =
await conn.FindAllOrdered<JsonDocument>(PostgresDb.TableName, [Field.Named("n:NumValue DESC")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one",
"The documents were not ordered correctly");
}),
TestCase("succeeds when ordering alphabetically", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var results = await conn.FindAllOrdered<JsonDocument>(PostgresDb.TableName, [Field.Named("Id DESC")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five",
"The documents were not ordered correctly");
})
]),
TestList("FindById",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -394,16 +440,17 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven"); var doc = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FindByField", new[] TestList("FindByFields",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var docs = await conn.FindByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "another")); var docs = await conn.FindByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
Expect.equal(docs.Count, 1, "There should have been one document returned"); Expect.equal(docs.Count, 1, "There should have been one document returned");
}), }),
TestCase("succeeds when documents are not found", async () => TestCase("succeeds when documents are not found", async () =>
@ -412,12 +459,40 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var docs = await conn.FindByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "mauve")); var docs = await conn.FindByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FindByContains", new[] TestList("FindByFieldsOrdered",
{ [
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("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");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("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");
})
]),
TestList("FindByContains",
[
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -437,9 +512,37 @@ public class PostgresCSharpExtensionTests
var docs = await conn.FindByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" }); var docs = await conn.FindByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FindByJsonPath", new[] TestList("FindByContainsOrdered",
{ [
// Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByContainsOrdered<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|four",
"Documents not ordered correctly");
}),
TestCase("succeeds when sorting descending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByContainsOrdered<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar DESC")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|two",
"Documents not ordered correctly");
})
]),
TestList("FindByJsonPath",
[
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -458,16 +561,45 @@ public class PostgresCSharpExtensionTests
var docs = await conn.FindByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); var docs = await conn.FindByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FindFirstByField", new[] TestList("FindByJsonPathOrdered",
{ [
// Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByJsonPathOrdered<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 15)",
[Field.Named("n:NumValue")]);
Expect.hasLength(docs, 3, "There should have been 3 documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "one|three|two",
"Documents not ordered correctly");
}),
TestCase("succeeds when sorting descending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.FindByJsonPathOrdered<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 15)",
[Field.Named("n:NumValue DESC")]);
Expect.hasLength(docs, 3, "There should have been 3 documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|three|one",
"Documents not ordered correctly");
})
]),
TestList("FindFirstByFields",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "another")); var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned"); Expect.equal(doc.Id, "two", "The incorrect document was returned");
}), }),
@ -477,9 +609,10 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "purple")); var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned");
}), }),
TestCase("succeeds when a document is not found", async () => TestCase("succeeds when a document is not found", async () =>
{ {
@ -487,12 +620,38 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "absent")); var doc = await conn.FindFirstByFields<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FindFirstByContains", new[] TestList("FindFirstByFieldsOrdered",
{ [
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("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");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("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");
})
]),
TestList("FindFirstByContains",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -512,7 +671,7 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName, var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } }); new { Sub = new { Foo = "green" } });
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned");
}), }),
TestCase("succeeds when a document is not found", async () => TestCase("succeeds when a document is not found", async () =>
{ {
@ -523,9 +682,34 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" }); var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" });
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FindFirstByJsonPath", new[] TestList("FindFirstByContainsOrdered",
{ [
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByContainsOrdered<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } }, [Field.Named("Value")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("two", doc.Id, "An incorrect document was returned");
}),
TestCase("succeeds when sorting descending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByContainsOrdered<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } }, [Field.Named("Value DESC")]);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal("four", doc.Id, "An incorrect document was returned");
})
]),
TestList("FindFirstByJsonPath",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -546,7 +730,7 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName, var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName,
"$.Sub.Foo ? (@ == \"green\")"); "$.Sub.Foo ? (@ == \"green\")");
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned");
}), }),
TestCase("succeeds when a document is not found", async () => TestCase("succeeds when a document is not found", async () =>
{ {
@ -557,9 +741,34 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("UpdateById", new[] TestList("FindFirstByJsonPathOrdered",
{ [
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByJsonPathOrdered<JsonDocument>(PostgresDb.TableName,
"$.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");
}),
TestCase("succeeds when sorting descending", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByJsonPathOrdered<JsonDocument>(PostgresDb.TableName,
"$.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");
})
]),
TestList("UpdateById",
[
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -588,9 +797,9 @@ public class PostgresCSharpExtensionTests
await conn.UpdateById(PostgresDb.TableName, "test", await conn.UpdateById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
}) })
}), ]),
TestList("UpdateByFunc", new[] TestList("UpdateByFunc",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -617,9 +826,9 @@ public class PostgresCSharpExtensionTests
await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id, await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
}) })
}), ]),
TestList("PatchById", new[] TestList("PatchById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -641,17 +850,19 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" }); await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" });
}) })
}), ]),
TestList("PatchByField", new[] TestList("PatchByFields",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
var after = await conn.CountByField(PostgresDb.TableName, Field.EQ("NumValue", "77")); new { NumValue = 77 });
var after = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any,
[Field.Equal("NumValue", 77)]);
Expect.equal(after, 2, "There should have been 2 documents returned"); Expect.equal(after, 2, "There should have been 2 documents returned");
}), }),
TestCase("succeeds when no document is updated", async () => TestCase("succeeds when no document is updated", async () =>
@ -662,11 +873,12 @@ public class PostgresCSharpExtensionTests
Expect.equal(before, 0, "There should have been no documents returned"); Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
new { Foo = "green" });
}) })
}), ]),
TestList("PatchByContains", new[] TestList("PatchByContains",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -687,9 +899,9 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
}) })
}), ]),
TestList("PatchByJsonPath", new[] TestList("PatchByJsonPath",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -710,16 +922,16 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
}) })
}), ]),
TestList("RemoveFieldsById", new[] TestList("RemoveFieldsById",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub", "Value" }); await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub", "Value"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "two"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "two");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.equal(updated.Value, "", "The string value should have been removed"); Expect.equal(updated.Value, "", "The string value should have been removed");
@ -731,7 +943,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub" }); await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "two"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "two");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.notEqual(updated.Value, "", "The string value should not have been removed"); Expect.notEqual(updated.Value, "", "The string value should not have been removed");
@ -744,7 +956,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["AFieldThatIsNotThere"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -752,19 +964,19 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Value" }); await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Value"]);
}) })
}), ]),
TestList("RemoveFieldsByField", new[] TestList("RemoveFieldsByFields",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
new[] { "Sub", "Value" }); ["Sub", "Value"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.equal(updated.Value, "", "The string value should have been removed"); Expect.equal(updated.Value, "", "The string value should have been removed");
@ -776,7 +988,8 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Sub" }); await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.notEqual(updated.Value, "", "The string value should not have been removed"); Expect.notEqual(updated.Value, "", "The string value should not have been removed");
@ -789,7 +1002,8 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Nothing" }); await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", "17")],
["Nothing"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -797,20 +1011,19 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any,
new[] { "Value" }); [Field.NotEqual("Abracadabra", "apple")], ["Value"]);
}) })
}), ]),
TestList("RemoveFieldsByContains", new[] TestList("RemoveFieldsByContains",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub", "Value"]);
new[] { "Sub", "Value" });
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.equal(updated.Value, "", "The string value should have been removed"); Expect.equal(updated.Value, "", "The string value should have been removed");
@ -822,7 +1035,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Sub" }); await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.notEqual(updated.Value, "", "The string value should not have been removed"); Expect.notEqual(updated.Value, "", "The string value should not have been removed");
@ -835,7 +1048,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Nothing" }); await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Nothing"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -843,20 +1056,18 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, ["Value"]);
new[] { "Value" });
}) })
}), ]),
TestList("RemoveFieldsByJsonPath", new[] TestList("RemoveFieldsByJsonPath",
{ [
TestCase("succeeds when multiple fields are removed", async () => TestCase("succeeds when multiple fields are removed", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub", "Value"]);
new[] { "Sub", "Value" });
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.equal(updated.Value, "", "The string value should have been removed"); Expect.equal(updated.Value, "", "The string value should have been removed");
@ -868,7 +1079,7 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Sub" }); await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub"]);
var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.notEqual(updated.Value, "", "The string value should not have been removed"); Expect.notEqual(updated.Value, "", "The string value should not have been removed");
@ -881,7 +1092,7 @@ public class PostgresCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Nothing" }); await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Nothing"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -889,12 +1100,11 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", ["Value"]);
new[] { "Value" });
}) })
}), ]),
TestList("DeleteById", new[] TestList("DeleteById",
{ [
TestCase("succeeds when a document is deleted", async () => TestCase("succeeds when a document is deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -915,16 +1125,16 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("DeleteByField", new[] TestList("DeleteByFields",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.DeleteByField(PostgresDb.TableName, Field.NE("Value", "purple")); await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NotEqual("Value", "purple")]);
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 2, "There should have been 2 documents remaining"); Expect.equal(remaining, 2, "There should have been 2 documents remaining");
}), }),
@ -934,13 +1144,13 @@ public class PostgresCSharpExtensionTests
await using var conn = MkConn(db); await using var conn = MkConn(db);
await LoadDocs(); await LoadDocs();
await conn.DeleteByField(PostgresDb.TableName, Field.EQ("Value", "crimson")); await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.Equal("Value", "crimson")]);
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("DeleteByContains", new[] TestList("DeleteByContains",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -961,9 +1171,9 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("DeleteByJsonPath", new[] TestList("DeleteByJsonPath",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
@ -984,6 +1194,6 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName); var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining"); Expect.equal(remaining, 5, "There should have been 5 documents remaining");
}) })
}), ]),
}); ]);
} }

File diff suppressed because it is too large Load Diff

View File

@ -131,7 +131,7 @@ public static class PostgresDb
var sqlProps = Sql.connect(database.ConnectionString); var sqlProps = Sql.connect(database.ConnectionString);
Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps)); Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps));
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps)); Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName, Dialect.PostgreSQL), sqlProps));
Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString)); Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString));

View File

@ -1,6 +1,5 @@
using Expecto.CSharp; using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.Data.Sqlite;
using BitBadger.Documents.Sqlite; using BitBadger.Documents.Sqlite;
namespace BitBadger.Documents.Tests.CSharp; namespace BitBadger.Documents.Tests.CSharp;
@ -18,10 +17,10 @@ public static class SqliteCSharpExtensionTests
/// Integration tests for the SQLite extension methods /// Integration tests for the SQLite extension methods
/// </summary> /// </summary>
[Tests] [Tests]
public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[] public static readonly Test Integration = TestList("Sqlite.C#.Extensions",
{ [
TestList("CustomSingle", new[] TestList("CustomSingle",
{ [
TestCase("succeeds when a row is found", async () => TestCase("succeeds when a row is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -29,7 +28,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("one") }, Results.FromData<JsonDocument>); [Parameters.Id("one")], Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "one", "The incorrect document was returned"); Expect.equal(doc!.Id, "one", "The incorrect document was returned");
}), }),
@ -40,19 +39,19 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>); [Parameters.Id("eighty")], Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("CustomList", new[] TestList("CustomList",
{ [
TestCase("succeeds when data is found", async () => TestCase("succeeds when data is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var docs = await conn.CustomList(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, var docs = await conn.CustomList(Query.Find(SqliteDb.TableName), Parameters.None,
Results.FromData<JsonDocument>); Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}), }),
@ -63,13 +62,13 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
var docs = await conn.CustomList( var docs = await conn.CustomList(
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", [new("@value", 100)],
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>); Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("CustomNonQuery", new[] TestList("CustomNonQuery",
{ [
TestCase("succeeds when operating on data", async () => TestCase("succeeds when operating on data", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -88,12 +87,12 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value",
new[] { new SqliteParameter("@value", 100) }); [new("@value", 100)]);
var remaining = await conn.CountAll(SqliteDb.TableName); var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table");
}) })
}), ]),
TestCase("CustomScalar succeeds", async () => TestCase("CustomScalar succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -107,41 +106,44 @@ public static class SqliteCSharpExtensionTests
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
Func<string, ValueTask<bool>> itExists = async name => var exists = await ItExists("ensured");
await conn.CustomScalar( var alsoExists = await ItExists("idx_ensured_key");
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it",
new SqliteParameter[] { new("@name", name) }, Results.ToExists);
var exists = await itExists("ensured");
var alsoExists = await itExists("idx_ensured_key");
Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already");
await conn.EnsureTable("ensured"); await conn.EnsureTable("ensured");
exists = await itExists("ensured"); exists = await ItExists("ensured");
alsoExists = await itExists("idx_ensured_key"); alsoExists = await ItExists("idx_ensured_key");
Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist"); Expect.isTrue(alsoExists, "The key index should now exist");
return;
Task<bool> ItExists(string name) =>
conn.CustomScalar($"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it",
[new("@name", name)], Results.ToExists);
}), }),
TestCase("EnsureFieldIndex succeeds", async () => TestCase("EnsureFieldIndex succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
var indexExists = () => conn.CustomScalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists(); var exists = await IndexExists();
Expect.isFalse(exists, "The index should not exist already"); Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured"); await conn.EnsureTable("ensured");
await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]);
exists = await indexExists(); exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist"); Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() =>
conn.CustomScalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
}), }),
TestList("Insert", new[] TestList("Insert",
{ [
TestCase("succeeds", async () => TestCase("succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -168,9 +170,9 @@ public static class SqliteCSharpExtensionTests
// This is what is supposed to happen // This is what is supposed to happen
} }
}) })
}), ]),
TestList("Save", new[] TestList("Save",
{ [
TestCase("succeeds when a document is inserted", async () => TestCase("succeeds when a document is inserted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -203,7 +205,7 @@ public static class SqliteCSharpExtensionTests
Expect.equal(after!.Id, "test", "The updated document is not correct"); Expect.equal(after!.Id, "test", "The updated document is not correct");
Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document");
}) })
}), ]),
TestCase("CountAll succeeds", async () => TestCase("CountAll succeeds", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -219,11 +221,12 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var theCount = await conn.CountByField(SqliteDb.TableName, Field.EQ("Value", "purple")); var theCount = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "purple")]);
Expect.equal(theCount, 2L, "There should have been 2 matching documents"); Expect.equal(theCount, 2L, "There should have been 2 matching documents");
}), }),
TestList("ExistsById", new[] TestList("ExistsById",
{ [
TestCase("succeeds when a document exists", async () => TestCase("succeeds when a document exists", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -242,16 +245,17 @@ public static class SqliteCSharpExtensionTests
var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); var exists = await conn.ExistsById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document"); Expect.isFalse(exists, "There should not have been an existing document");
}) })
}), ]),
TestList("ExistsByField", new[] TestList("ExistsByFields",
{ [
TestCase("succeeds when documents exist", async () => TestCase("succeeds when documents exist", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var exists = await conn.ExistsByField(SqliteDb.TableName, Field.GE("NumValue", 10)); var exists = await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.GreaterOrEqual("NumValue", 10)]);
Expect.isTrue(exists, "There should have been existing documents"); Expect.isTrue(exists, "There should have been existing documents");
}), }),
TestCase("succeeds when no matching documents exist", async () => TestCase("succeeds when no matching documents exist", async () =>
@ -260,12 +264,13 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var exists = await conn.ExistsByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); var exists =
await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Nothing", "none")]);
Expect.isFalse(exists, "There should not have been any existing documents"); Expect.isFalse(exists, "There should not have been any existing documents");
}) })
}), ]),
TestList("FindAll", new[] TestList("FindAll",
{ [
TestCase("succeeds when there is data", async () => TestCase("succeeds when there is data", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -285,9 +290,46 @@ public static class SqliteCSharpExtensionTests
var results = await conn.FindAll<JsonDocument>(SqliteDb.TableName); var results = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned"); Expect.isEmpty(results, "There should have been no documents returned");
}) })
}), ]),
TestList("FindById", new[] TestList("FindAllOrdered",
{ [
TestCase("succeeds when ordering numerically", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var results = await conn.FindAllOrdered<JsonDocument>(SqliteDb.TableName, [Field.Named("n:NumValue")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five",
"The documents were not ordered correctly");
}),
TestCase("succeeds when ordering numerically descending", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var results =
await conn.FindAllOrdered<JsonDocument>(SqliteDb.TableName, [Field.Named("n:NumValue DESC")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one",
"The documents were not ordered correctly");
}),
TestCase("succeeds when ordering alphabetically", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var results = await conn.FindAllOrdered<JsonDocument>(SqliteDb.TableName, [Field.Named("Id DESC")]);
Expect.hasLength(results, 5, "There should have been 5 documents returned");
Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five",
"The documents were not ordered correctly");
})
]),
TestList("FindById",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -307,16 +349,17 @@ public static class SqliteCSharpExtensionTests
var doc = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "eighty-seven"); var doc = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "eighty-seven");
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("FindByField", new[] TestList("FindByFields",
{ [
TestCase("succeeds when documents are found", async () => TestCase("succeeds when documents are found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var docs = await conn.FindByField<JsonDocument>(SqliteDb.TableName, Field.GT("NumValue", 15)); var docs = await conn.FindByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)]);
Expect.equal(docs.Count, 2, "There should have been two documents returned"); Expect.equal(docs.Count, 2, "There should have been two documents returned");
}), }),
TestCase("succeeds when documents are not found", async () => TestCase("succeeds when documents are not found", async () =>
@ -325,19 +368,46 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var docs = await conn.FindByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve")); var docs = await conn.FindByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "mauve")]);
Expect.isEmpty(docs, "There should have been no documents returned"); Expect.isEmpty(docs, "There should have been no documents returned");
}) })
}), ]),
TestList("FindFirstByField", new[] TestList("ByFieldsOrdered",
{ [
TestCase("succeeds when documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var docs = await conn.FindByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Greater("NumValue", 15)], [Field.Named("Id DESC")]);
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"There should have been two documents returned");
})
]),
TestList("FindFirstByFields",
[
TestCase("succeeds when a document is found", async () => TestCase("succeeds when a document is found", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "another")); var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "another")]);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}), }),
@ -347,9 +417,10 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Sub.Foo", "green")); var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Sub.Foo", "green")]);
Expect.isNotNull(doc, "There should have been a document returned"); Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned");
}), }),
TestCase("succeeds when a document is not found", async () => TestCase("succeeds when a document is not found", async () =>
{ {
@ -357,12 +428,38 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent")); var doc = await conn.FindFirstByFields<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("Value", "absent")]);
Expect.isNull(doc, "There should not have been a document returned"); Expect.isNull(doc, "There should not have been a document returned");
}) })
}), ]),
TestList("UpdateById", new[] TestList("FindFirstByFieldsOrdered",
{ [
TestCase("succeeds when sorting ascending", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("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");
}),
TestCase("succeeds when sorting descending", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindFirstByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.Equal("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");
})
]),
TestList("UpdateById",
[
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -389,9 +486,9 @@ public static class SqliteCSharpExtensionTests
await conn.UpdateById(SqliteDb.TableName, "test", await conn.UpdateById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
}) })
}), ]),
TestList("UpdateByFunc", new[] TestList("UpdateByFunc",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -418,9 +515,9 @@ public static class SqliteCSharpExtensionTests
await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id, await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
}) })
}), ]),
TestList("PatchById", new[] TestList("PatchById",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -443,17 +540,18 @@ public static class SqliteCSharpExtensionTests
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" }); await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" });
}) })
}), ]),
TestList("PatchByField", new[] TestList("PatchByFields",
{ [
TestCase("succeeds when a document is updated", async () => TestCase("succeeds when a document is updated", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "purple")],
var after = await conn.CountByField(SqliteDb.TableName, Field.EQ("NumValue", 77)); new { NumValue = 77 });
var after = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 77)]);
Expect.equal(after, 2L, "There should have been 2 documents returned"); Expect.equal(after, 2L, "There should have been 2 documents returned");
}), }),
TestCase("succeeds when no document is updated", async () => TestCase("succeeds when no document is updated", async () =>
@ -464,18 +562,19 @@ public static class SqliteCSharpExtensionTests
Expect.isEmpty(before, "There should have been no documents returned"); Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test // This not raising an exception is the test
await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "burgundy")],
new { Foo = "green" });
}) })
}), ]),
TestList("RemoveFieldsById", new[] TestList("RemoveFieldsById",
{ [
TestCase("succeeds when fields are removed", async () => TestCase("succeeds when fields are removed", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Sub", "Value" }); await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Sub", "Value"]);
var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "two"); var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "two");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.equal(updated.Value, "", "The string value should have been removed"); Expect.equal(updated.Value, "", "The string value should have been removed");
@ -488,7 +587,7 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["AFieldThatIsNotThere"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -496,18 +595,19 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Value" }); await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Value"]);
}) })
}), ]),
TestList("RemoveFieldsByField", new[] TestList("RemoveFieldsByFields",
{ [
TestCase("succeeds when a field is removed", async () => TestCase("succeeds when a field is removed", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Sub" }); await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)],
["Sub"]);
var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "four"); var updated = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "four");
Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNotNull(updated, "The updated document should have been retrieved");
Expect.isNull(updated.Sub, "The sub-document should have been removed"); Expect.isNull(updated.Sub, "The sub-document should have been removed");
@ -519,7 +619,8 @@ public static class SqliteCSharpExtensionTests
await LoadDocs(); await LoadDocs();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Nothing" }); await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("NumValue", 17)],
["Nothing"]);
}), }),
TestCase("succeeds when no document is matched", async () => TestCase("succeeds when no document is matched", async () =>
{ {
@ -527,11 +628,12 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
// This not raising an exception is the test // This not raising an exception is the test
await conn.RemoveFieldsByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any,
[Field.NotEqual("Abracadabra", "apple")], ["Value"]);
}) })
}), ]),
TestList("DeleteById", new[] TestList("DeleteById",
{ [
TestCase("succeeds when a document is deleted", async () => TestCase("succeeds when a document is deleted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
@ -552,16 +654,16 @@ public static class SqliteCSharpExtensionTests
var remaining = await conn.CountAll(SqliteDb.TableName); var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
}) })
}), ]),
TestList("DeleteByField", new[] TestList("DeleteByFields",
{ [
TestCase("succeeds when documents are deleted", async () => TestCase("succeeds when documents are deleted", async () =>
{ {
await using var db = await SqliteDb.BuildDb(); await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
await conn.DeleteByField(SqliteDb.TableName, Field.NE("Value", "purple")); await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NotEqual("Value", "purple")]);
var remaining = await conn.CountAll(SqliteDb.TableName); var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); Expect.equal(remaining, 2L, "There should have been 2 documents remaining");
}), }),
@ -571,11 +673,11 @@ public static class SqliteCSharpExtensionTests
await using var conn = Sqlite.Configuration.DbConn(); await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs(); await LoadDocs();
await conn.DeleteByField(SqliteDb.TableName, Field.EQ("Value", "crimson")); await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.Equal("Value", "crimson")]);
var remaining = await conn.CountAll(SqliteDb.TableName); var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
}) })
}), ]),
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
}); ]);
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,11 @@
namespace BitBadger.Documents.Tests.CSharp; namespace BitBadger.Documents.Tests.CSharp;
public class NumIdDocument
{
public int Key { get; set; } = 0;
public string Text { get; set; } = "";
}
public class SubDocument public class SubDocument
{ {
public string Foo { get; set; } = ""; public string Foo { get; set; } = "";
@ -12,4 +18,32 @@ public class JsonDocument
public string Value { get; set; } = ""; public string Value { get; set; } = "";
public int NumValue { get; set; } = 0; public int NumValue { get; set; } = 0;
public SubDocument? Sub { get; set; } = null; 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

@ -2,11 +2,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<NoWarn>1182</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="CommonTests.fs" />
<Compile Include="Types.fs" /> <Compile Include="Types.fs" />
<Compile Include="CommonTests.fs" />
<Compile Include="PostgresTests.fs" /> <Compile Include="PostgresTests.fs" />
<Compile Include="PostgresExtensionTests.fs" /> <Compile Include="PostgresExtensionTests.fs" />
<Compile Include="SqliteTests.fs" /> <Compile Include="SqliteTests.fs" />

View File

@ -6,135 +6,491 @@ open Expecto
/// Test table name /// Test table name
let tbl = "test_table" let tbl = "test_table"
/// Tests which do not hit the database /// Unit tests for the Op DU
let all = let comparisonTests = testList "Comparison.OpSql" [
testList "Common" [ test "Equal succeeds" {
testList "Op" [ Expect.equal (Equal "").OpSql "=" "The Equals SQL was not correct"
test "EQ succeeds" { }
Expect.equal (string EQ) "=" "The equals operator was not correct" test "Greater succeeds" {
} Expect.equal (Greater "").OpSql ">" "The Greater SQL was not correct"
test "GT succeeds" { }
Expect.equal (string GT) ">" "The greater than operator was not correct" test "GreaterOrEqual succeeds" {
} Expect.equal (GreaterOrEqual "").OpSql ">=" "The GreaterOrEqual SQL was not correct"
test "GE succeeds" { }
Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct" test "Less succeeds" {
} Expect.equal (Less "").OpSql "<" "The Less SQL was not correct"
test "LT succeeds" { }
Expect.equal (string LT) "<" "The less than operator was not correct" test "LessOrEqual succeeds" {
} Expect.equal (LessOrEqual "").OpSql "<=" "The LessOrEqual SQL was not correct"
test "LE succeeds" { }
Expect.equal (string LE) "<=" "The less than or equal to operator was not correct" test "NotEqual succeeds" {
} Expect.equal (NotEqual "").OpSql "<>" "The NotEqual SQL was not correct"
test "NE succeeds" { }
Expect.equal (string NE) "<>" "The not equal to operator was not correct" test "Between succeeds" {
} Expect.equal (Between("", "")).OpSql "BETWEEN" "The Between SQL was not correct"
test "BT succeeds" { }
Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct""" test "In succeeds" {
} Expect.equal (In []).OpSql "IN" "The In SQL was not correct"
test "EX succeeds" { }
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" test "InArray succeeds" {
} Expect.equal (InArray("", [])).OpSql "?|" "The InArray SQL was not correct"
test "NEX succeeds" { }
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" test "Exists succeeds" {
} Expect.equal Exists.OpSql "IS NOT NULL" "The Exists SQL was not correct"
] }
testList "Field" [ test "NotExists succeeds" {
test "EQ succeeds" { Expect.equal NotExists.OpSql "IS NULL" "The NotExists SQL was not correct"
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" /// Unit tests for the Field class
} let fieldTests = testList "Field" [
test "GT succeeds" { test "Equal succeeds" {
let field = Field.GT "Great" "night" let field = Field.Equal "Test" 14
Expect.equal field.Name "Great" "Field name incorrect" Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Op GT "Operator incorrect" Expect.equal field.Comparison (Equal 14) "Comparison 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 test "Greater succeeds" {
Expect.equal field.Name "Nice" "Field name incorrect" let field = Field.Greater "Great" "night"
Expect.equal field.Op GE "Operator incorrect" Expect.equal field.Name "Great" "Field name incorrect"
Expect.equal field.Value 88L "Value incorrect" Expect.equal field.Comparison (Greater "night") "Comparison incorrect"
} Expect.isNone field.ParameterName "The default parameter name should be None"
test "LT succeeds" { Expect.isNone field.Qualifier "The default table qualifier should be None"
let field = Field.LT "Lesser" "seven" }
Expect.equal field.Name "Lesser" "Field name incorrect" test "GreaterOrEqual succeeds" {
Expect.equal field.Op LT "Operator incorrect" let field = Field.GreaterOrEqual "Nice" 88L
Expect.equal field.Value "seven" "Value incorrect" Expect.equal field.Name "Nice" "Field name incorrect"
} Expect.equal field.Comparison (GreaterOrEqual 88L) "Comparison incorrect"
test "LE succeeds" { Expect.isNone field.ParameterName "The default parameter name should be None"
let field = Field.LE "Nobody" "KNOWS"; Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Name "Nobody" "Field name incorrect" }
Expect.equal field.Op LE "Operator incorrect" test "Less succeeds" {
Expect.equal field.Value "KNOWS" "Value incorrect" let field = Field.Less "Lesser" "seven"
} Expect.equal field.Name "Lesser" "Field name incorrect"
test "NE succeeds" { Expect.equal field.Comparison (Less "seven") "Comparison incorrect"
let field = Field.NE "Park" "here" Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.equal field.Name "Park" "Field name incorrect" Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op NE "Operator incorrect" }
Expect.equal field.Value "here" "Value incorrect" test "LessOrEqual succeeds" {
} let field = Field.LessOrEqual "Nobody" "KNOWS";
test "BT succeeds" { Expect.equal field.Name "Nobody" "Field name incorrect"
let field = Field.BT "Age" 18 49 Expect.equal field.Comparison (LessOrEqual "KNOWS") "Comparison incorrect"
Expect.equal field.Name "Age" "Field name incorrect" Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.equal field.Op BT "Operator incorrect" Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" }
} test "NotEqual succeeds" {
test "EX succeeds" { let field = Field.NotEqual "Park" "here"
let field = Field.EX "Groovy" Expect.equal field.Name "Park" "Field name incorrect"
Expect.equal field.Name "Groovy" "Field name incorrect" Expect.equal field.Comparison (NotEqual "here") "Comparison 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" test "Between succeeds" {
Expect.equal field.Name "Rad" "Field name incorrect" let field = Field.Between "Age" 18 49
Expect.equal field.Op NEX "Operator incorrect" 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"
testList "Query" [ Expect.isNone field.Qualifier "The default table qualifier should be None"
test "selectFromTable succeeds" { }
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" test "In succeeds" {
} let field = Field.In "Here" [| 8; 16; 32 |]
testList "Definition" [ Expect.equal field.Name "Here" "Field name incorrect"
test "ensureTableFor succeeds" { match field.Comparison with
Expect.equal | In values -> Expect.equal (List.ofSeq values) [ box 8; box 16; box 32 ] "Comparison incorrect"
(Query.Definition.ensureTableFor "my.table" "JSONB") | it -> Expect.isTrue false $"Expected In, received %A{it}"
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)" Expect.isNone field.ParameterName "The default parameter name should be None"
"CREATE TABLE statement not constructed correctly" 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>
} }
testList "ensureKey" [
test "succeeds when a schema is present" { let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |}
Expect.equal Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used"
(Query.Definition.ensureKey "test.table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))" let deserialized = Configuration.serializer().Deserialize<obj> """{"Something":"here"}"""
"CREATE INDEX for key statement with schema not constructed correctly" Expect.isNull deserialized "Specified serializer should have returned null"
} finally
test "succeeds when a schema is not present" { Configuration.useSerializer DocumentSerializer.``default``
Expect.equal }
(Query.Definition.ensureKey "table") test "serializer returns configured serializer" {
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))" Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ()))
"CREATE INDEX for key statement without schema not constructed correctly" "Serializer should have been the same"
} }
] test "useIdField / idField succeeds" {
test "ensureIndexOn succeeds for multiple fields and directions" { try
Expect.equal Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect"
(Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ]) Configuration.useIdField "id"
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field"
"((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)" ] finally
|> String.concat "") Configuration.useIdField "Id"
"CREATE INDEX for multiple field statement incorrect" }
} test "useAutoIdStrategy / autoIdStrategy succeeds" {
] try
test "insert succeeds" { Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect"
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" Configuration.useAutoIdStrategy Guid
} Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly"
test "save succeeds" { 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 Expect.equal
(Query.save tbl) (Query.Definition.ensureKey "test.table" PostgreSQL)
$"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data" "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))"
"INSERT ON CONFLICT UPDATE statement not correct" "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
]

View File

@ -25,7 +25,7 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! docs = conn.customList (Query.selectFromTable PostgresDb.TableName) [] fromData<JsonDocument> let! docs = conn.customList (Query.find PostgresDb.TableName) [] fromData<JsonDocument>
Expect.equal (List.length docs) 5 "There should have been 5 documents returned" Expect.equal (List.length docs) 5 "There should have been 5 documents returned"
} }
testTask "succeeds when data is not found" { testTask "succeeds when data is not found" {
@ -209,12 +209,12 @@ let integrationTests =
let! theCount = conn.countAll PostgresDb.TableName let! theCount = conn.countAll PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents" Expect.equal theCount 5 "There should have been 5 matching documents"
} }
testTask "countByField succeeds" { testTask "countByFields succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! theCount = conn.countByField PostgresDb.TableName (Field.EQ "Value" "purple") let! theCount = conn.countByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
Expect.equal theCount 2 "There should have been 2 matching documents" Expect.equal theCount 2 "There should have been 2 matching documents"
} }
testTask "countByContains succeeds" { testTask "countByContains succeeds" {
@ -251,13 +251,13 @@ let integrationTests =
Expect.isFalse exists "There should not have been an existing document" Expect.isFalse exists "There should not have been an existing document"
} }
] ]
testList "existsByField" [ testList "existsByFields" [
testTask "succeeds when documents exist" { testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! exists = conn.existsByField PostgresDb.TableName (Field.EX "Sub") let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.Exists "Sub" ]
Expect.isTrue exists "There should have been existing documents" Expect.isTrue exists "There should have been existing documents"
} }
testTask "succeeds when documents do not exist" { testTask "succeeds when documents do not exist" {
@ -265,7 +265,7 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! exists = conn.existsByField PostgresDb.TableName (Field.EQ "NumValue" "six") let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "six" ]
Expect.isFalse exists "There should not have been existing documents" Expect.isFalse exists "There should not have been existing documents"
} }
] ]
@ -315,12 +315,10 @@ let integrationTests =
do! conn.insert PostgresDb.TableName { Foo = "five"; Bar = "six" } do! conn.insert PostgresDb.TableName { Foo = "five"; Bar = "six" }
let! results = conn.findAll<SubDocument> PostgresDb.TableName let! results = conn.findAll<SubDocument> PostgresDb.TableName
let expected = [ Expect.equal
{ Foo = "one"; Bar = "two" } results
{ Foo = "three"; Bar = "four" } [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ]
{ Foo = "five"; Bar = "six" } "There should have been 3 documents returned"
]
Expect.equal results expected "There should have been 3 documents returned"
} }
testTask "succeeds when there is no data" { testTask "succeeds when there is no data" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -329,6 +327,44 @@ let integrationTests =
Expect.equal results [] "There should have been no documents returned" Expect.equal results [] "There should have been no documents returned"
} }
] ]
testList "findAllOrdered" [
testTask "succeeds when ordering numerically" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! results = conn.findAllOrdered<JsonDocument> PostgresDb.TableName [ Field.Named "n:NumValue" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"one|three|two|four|five"
"The documents were not ordered correctly"
}
testTask "succeeds when ordering numerically descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! results = conn.findAllOrdered<JsonDocument> PostgresDb.TableName [ Field.Named "n:NumValue DESC" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"five|four|two|three|one"
"The documents were not ordered correctly"
}
testTask "succeeds when ordering alphabetically" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! results = conn.findAllOrdered<JsonDocument> PostgresDb.TableName [ Field.Named "Id DESC" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"two|three|one|four|five"
"The documents were not ordered correctly"
}
]
testList "findById" [ testList "findById" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -348,13 +384,13 @@ let integrationTests =
Expect.isNone doc "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
} }
] ]
testList "findByField" [ testList "findByFields" [
testTask "succeeds when documents are found" { testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! docs = conn.findByField<JsonDocument> PostgresDb.TableName (Field.EQ "Value" "another") let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "another" ]
Expect.equal (List.length docs) 1 "There should have been one document returned" Expect.equal (List.length docs) 1 "There should have been one document returned"
} }
testTask "succeeds when documents are not found" { testTask "succeeds when documents are not found" {
@ -362,10 +398,36 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! docs = conn.findByField<JsonDocument> PostgresDb.TableName (Field.EQ "Value" "mauve") let! docs = conn.findByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "mauve" ]
Expect.isEmpty docs "There should have been no documents returned" Expect.isEmpty docs "There should have been no documents returned"
} }
] ]
testList "findByFieldsOrdered" [
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "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"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByFieldsOrdered<JsonDocument>
PostgresDb.TableName All [ Field.Equal "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"
}
]
testList "findByContains" [ testList "findByContains" [
testTask "succeeds when documents are found" { testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -384,6 +446,33 @@ let integrationTests =
Expect.isEmpty docs "There should have been no documents returned" Expect.isEmpty docs "There should have been no documents returned"
} }
] ]
testList "findByContainsOrdered" [
// Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByContainsOrdered<JsonDocument>
PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "two|four" "Documents not ordered correctly"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByContainsOrdered<JsonDocument>
PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar DESC" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|two" "Documents not ordered correctly"
}
]
testList "findByJsonPath" [ testList "findByJsonPath" [
testTask "succeeds when documents are found" { testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -402,13 +491,41 @@ let integrationTests =
Expect.isEmpty docs "There should have been no documents returned" Expect.isEmpty docs "There should have been no documents returned"
} }
] ]
testList "findFirstByField" [ testList "findByJsonPathOrdered" [
// Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByJsonPathOrdered<JsonDocument>
PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue" ]
Expect.hasLength docs 3 "There should have been 3 documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "one|three|two" "Documents not ordered correctly"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.findByJsonPathOrdered<JsonDocument>
PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue DESC" ]
Expect.hasLength docs 3 "There should have been 3 documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "two|three|one" "Documents not ordered correctly"
}
]
testList "findFirstByFields" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName (Field.EQ "Value" "another") let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "another" ]
Expect.isSome doc "There should have been a document returned" Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned"
} }
@ -417,7 +534,8 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName (Field.EQ "Value" "purple") let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
Expect.isSome doc "There should have been a document returned" Expect.isSome doc "There should have been a document returned"
Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned"
} }
@ -426,10 +544,35 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName (Field.EQ "Value" "absent") let! doc =
conn.findFirstByFields<JsonDocument> PostgresDb.TableName Any [ Field.Equal "Value" "absent" ]
Expect.isNone doc "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
} }
] ]
testList "findFirstByFieldsOrdered" [
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "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"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
PostgresDb.TableName Any [ Field.Equal "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"
}
]
testList "findFirstByContains" [ testList "findFirstByContains" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -458,6 +601,30 @@ let integrationTests =
Expect.isNone doc "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
} }
] ]
testList "findFirstByContainsOrdered" [
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByContainsOrdered<JsonDocument>
PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "two" doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByContainsOrdered<JsonDocument>
PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value DESC" ]
Expect.isSome doc "There should have been a document returned"
Expect.equal "four" doc.Value.Id "An incorrect document was returned"
}
]
testList "findFirstByJsonPath" [ testList "findFirstByJsonPath" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -486,6 +653,30 @@ let integrationTests =
Expect.isNone doc "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
} }
] ]
testList "findFirstByJsonPathOrdered" [
testTask "succeeds when sorting ascending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByJsonPathOrdered<JsonDocument>
PostgresDb.TableName """$.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"
}
testTask "succeeds when sorting descending" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.findFirstByJsonPathOrdered<JsonDocument>
PostgresDb.TableName """$.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"
}
]
testList "updateById" [ testList "updateById" [
testTask "succeeds when a document is updated" { testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -556,14 +747,14 @@ let integrationTests =
do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |} do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |}
} }
] ]
testList "patchByField" [ testList "patchByFields" [
testTask "succeeds when a document is updated" { testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} do! conn.patchByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByField PostgresDb.TableName (Field.EQ "NumValue" "77") let! after = conn.countByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "77" ]
Expect.equal after 2 "There should have been 2 documents returned" Expect.equal after 2 "There should have been 2 documents returned"
} }
testTask "succeeds when no document is updated" { testTask "succeeds when no document is updated" {
@ -573,7 +764,7 @@ let integrationTests =
Expect.equal before 0 "There should have been no documents returned" Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test // This not raising an exception is the test
do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} do! conn.patchByFields PostgresDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
} }
] ]
testList "patchByContains" [ testList "patchByContains" [
@ -623,9 +814,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub"; "Value" ] do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub"; "Value" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields" Expect.equal noValue 1 "There should be 1 document without Value fields"
} }
testTask "succeeds when a single field is removed" { testTask "succeeds when a single field is removed" {
@ -634,9 +825,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub" ] do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields" Expect.equal noValue 0 "There should be no documents without Value fields"
} }
testTask "succeeds when a field is not removed" { testTask "succeeds when a field is not removed" {
@ -655,16 +846,17 @@ let integrationTests =
do! conn.removeFieldsById PostgresDb.TableName "two" [ "Value" ] do! conn.removeFieldsById PostgresDb.TableName "two" [ "Value" ]
} }
] ]
testList "removeFieldsByField" [ testList "removeFieldsByFields" [
testTask "succeeds when multiple fields are removed" { testTask "succeeds when multiple fields are removed" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub"; "Value" ] do! conn.removeFieldsByFields
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Sub"; "Value" ]
let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields" Expect.equal noValue 1 "There should be 1 document without Value fields"
} }
testTask "succeeds when a single field is removed" { testTask "succeeds when a single field is removed" {
@ -672,10 +864,10 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub" ] do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Sub" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields" Expect.equal noValue 0 "There should be no documents without Value fields"
} }
testTask "succeeds when a field is not removed" { testTask "succeeds when a field is not removed" {
@ -684,14 +876,15 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
// This not raising an exception is the test // This not raising an exception is the test
do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Nothing" ] do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.Equal "NumValue" "17" ] [ "Nothing" ]
} }
testTask "succeeds when no document is matched" { testTask "succeeds when no document is matched" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
// This not raising an exception is the test // This not raising an exception is the test
do! conn.removeFieldsByField PostgresDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] do! conn.removeFieldsByFields
PostgresDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
} }
] ]
testList "removeFieldsByContains" [ testList "removeFieldsByContains" [
@ -701,9 +894,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields" Expect.equal noValue 1 "There should be 1 document without Value fields"
} }
testTask "succeeds when a single field is removed" { testTask "succeeds when a single field is removed" {
@ -712,9 +905,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields" Expect.equal noValue 0 "There should be no documents without Value fields"
} }
testTask "succeeds when a field is not removed" { testTask "succeeds when a field is not removed" {
@ -740,9 +933,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 1 "There should be 1 document without Value fields" Expect.equal noValue 1 "There should be 1 document without Value fields"
} }
testTask "succeeds when a single field is removed" { testTask "succeeds when a single field is removed" {
@ -751,9 +944,9 @@ let integrationTests =
do! loadDocs conn do! loadDocs conn
do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ]
let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Sub" ]
Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" Expect.equal noSubs 4 "There should now be 4 documents without Sub fields"
let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NotExists "Value" ]
Expect.equal noValue 0 "There should be no documents without Value fields" Expect.equal noValue 0 "There should be no documents without Value fields"
} }
testTask "succeeds when a field is not removed" { testTask "succeeds when a field is not removed" {
@ -792,13 +985,13 @@ let integrationTests =
Expect.equal remaining 5 "There should have been 5 documents remaining" Expect.equal remaining 5 "There should have been 5 documents remaining"
} }
] ]
testList "deleteByField" [ testList "deleteByFields" [
testTask "succeeds when documents are deleted" { testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "purple") do! conn.deleteByFields PostgresDb.TableName Any [ Field.Equal "Value" "purple" ]
let! remaining = conn.countAll PostgresDb.TableName let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining" Expect.equal remaining 3 "There should have been 3 documents remaining"
} }
@ -807,7 +1000,7 @@ let integrationTests =
use conn = mkConn db use conn = mkConn db
do! loadDocs conn do! loadDocs conn
do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "crimson") do! conn.deleteByFields PostgresDb.TableName Any [ Field.Equal "Value" "crimson" ]
let! remaining = conn.countAll PostgresDb.TableName let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining" Expect.equal remaining 5 "There should have been 5 documents remaining"
} }

File diff suppressed because it is too large Load Diff

View File

@ -113,12 +113,12 @@ let integrationTests =
let! theCount = conn.countAll SqliteDb.TableName let! theCount = conn.countAll SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents" Expect.equal theCount 5L "There should have been 5 matching documents"
} }
testTask "countByField succeeds" { testTask "countByFields succeeds" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! theCount = conn.countByField SqliteDb.TableName (Field.EQ "Value" "purple") let! theCount = conn.countByFields SqliteDb.TableName Any [ Field.Equal "Value" "purple" ]
Expect.equal theCount 2L "There should have been 2 matching documents" Expect.equal theCount 2L "There should have been 2 matching documents"
} }
testList "existsById" [ testList "existsById" [
@ -139,13 +139,13 @@ let integrationTests =
Expect.isFalse exists "There should not have been an existing document" Expect.isFalse exists "There should not have been an existing document"
} }
] ]
testList "existsByField" [ testList "existsByFields" [
testTask "succeeds when documents exist" { testTask "succeeds when documents exist" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "NumValue" 10) let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 10 ]
Expect.isTrue exists "There should have been existing documents" Expect.isTrue exists "There should have been existing documents"
} }
testTask "succeeds when no matching documents exist" { testTask "succeeds when no matching documents exist" {
@ -153,7 +153,7 @@ let integrationTests =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "Nothing" "none") let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.Equal "Nothing" "none" ]
Expect.isFalse exists "There should not have been any existing documents" Expect.isFalse exists "There should not have been any existing documents"
} }
] ]
@ -181,6 +181,44 @@ let integrationTests =
Expect.equal results [] "There should have been no documents returned" Expect.equal results [] "There should have been no documents returned"
} }
] ]
testList "findAllOrdered" [
testTask "succeeds when ordering numerically" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! results = conn.findAllOrdered<JsonDocument> SqliteDb.TableName [ Field.Named "n:NumValue" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"one|three|two|four|five"
"The documents were not ordered correctly"
}
testTask "succeeds when ordering numerically descending" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! results = conn.findAllOrdered<JsonDocument> SqliteDb.TableName [ Field.Named "n:NumValue DESC" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"five|four|two|three|one"
"The documents were not ordered correctly"
}
testTask "succeeds when ordering alphabetically" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! results = conn.findAllOrdered<JsonDocument> SqliteDb.TableName [ Field.Named "Id DESC" ]
Expect.hasLength results 5 "There should have been 5 documents returned"
Expect.equal
(results |> List.map _.Id |> String.concat "|")
"two|three|one|four|five"
"The documents were not ordered correctly"
}
]
testList "findById" [ testList "findById" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
@ -188,7 +226,7 @@ let integrationTests =
do! loadDocs () do! loadDocs ()
let! doc = conn.findById<string, JsonDocument> SqliteDb.TableName "two" let! doc = conn.findById<string, JsonDocument> SqliteDb.TableName "two"
Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned"
} }
testTask "succeeds when a document is not found" { testTask "succeeds when a document is not found" {
@ -197,35 +235,59 @@ let integrationTests =
do! loadDocs () do! loadDocs ()
let! doc = conn.findById<string, JsonDocument> SqliteDb.TableName "three hundred eighty-seven" let! doc = conn.findById<string, JsonDocument> SqliteDb.TableName "three hundred eighty-seven"
Expect.isFalse (Option.isSome doc) "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
} }
] ]
testList "findByField" [ testList "findByFields" [
testTask "succeeds when documents are found" { testTask "succeeds when documents are found" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! docs = conn.findByField<JsonDocument> SqliteDb.TableName (Field.EQ "Sub.Foo" "green") let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ]
Expect.equal (List.length docs) 2 "There should have been two documents returned" Expect.hasLength docs 2 "There should have been two documents returned"
} }
testTask "succeeds when documents are not found" { testTask "succeeds when documents are not found" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! docs = conn.findByField<JsonDocument> SqliteDb.TableName (Field.EQ "Value" "mauve") let! docs = conn.findByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "mauve" ]
Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" Expect.isEmpty docs "There should have been no documents returned"
} }
] ]
testList "findFirstByField" [ testList "findByFieldsOrdered" [
testTask "succeeds when sorting ascending" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs =
conn.findByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly"
}
testTask "succeeds when sorting descending" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs =
conn.findByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Greater "NumValue" 15 ] [ Field.Named "Id DESC" ]
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly"
}
]
testList "findFirstByFields" [
testTask "succeeds when a document is found" { testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName (Field.EQ "Value" "another") let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "another" ]
Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned"
} }
testTask "succeeds when multiple documents are found" { testTask "succeeds when multiple documents are found" {
@ -233,8 +295,8 @@ let integrationTests =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName (Field.EQ "Sub.Foo" "green") let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Sub.Foo" "green" ]
Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
} }
testTask "succeeds when a document is not found" { testTask "succeeds when a document is not found" {
@ -242,8 +304,32 @@ let integrationTests =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName (Field.EQ "Value" "absent") let! doc = conn.findFirstByFields<JsonDocument> SqliteDb.TableName Any [ Field.Equal "Value" "absent" ]
Expect.isFalse (Option.isSome doc) "There should not have been a document returned" Expect.isNone doc "There should not have been a document returned"
}
]
testList "findFirstByFieldsOrdered" [
testTask "succeeds when sorting ascending" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "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"
}
testTask "succeeds when sorting descending" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc =
conn.findFirstByFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.Equal "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"
} }
] ]
testList "updateById" [ testList "updateById" [
@ -324,14 +410,14 @@ let integrationTests =
do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |} do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |}
} }
] ]
testList "patchByField" [ testList "patchByFields" [
testTask "succeeds when a document is updated" { testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} do! conn.patchByFields SqliteDb.TableName Any [ Field.Equal "Value" "purple" ] {| NumValue = 77 |}
let! after = conn.countByField SqliteDb.TableName (Field.EQ "NumValue" 77) let! after = conn.countByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 77 ]
Expect.equal after 2L "There should have been 2 documents returned" Expect.equal after 2L "There should have been 2 documents returned"
} }
testTask "succeeds when no document is updated" { testTask "succeeds when no document is updated" {
@ -342,7 +428,7 @@ let integrationTests =
Expect.isEmpty before "There should have been no documents returned" Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test // This not raising an exception is the test
do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} do! conn.patchByFields SqliteDb.TableName Any [ Field.Equal "Value" "burgundy" ] {| Foo = "green" |}
} }
] ]
testList "removeFieldsById" [ testList "removeFieldsById" [
@ -375,13 +461,13 @@ let integrationTests =
do! conn.removeFieldsById SqliteDb.TableName "two" [ "Value" ] do! conn.removeFieldsById SqliteDb.TableName "two" [ "Value" ]
} }
] ]
testList "removeFieldByField" [ testList "removeFieldByFields" [
testTask "succeeds when a field is removed" { testTask "succeeds when a field is removed" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Sub" ] do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Sub" ]
try try
let! _ = conn.findById<string, JsonDocument> SqliteDb.TableName "four" let! _ = conn.findById<string, JsonDocument> SqliteDb.TableName "four"
Expect.isTrue false "The updated document should have failed to parse" Expect.isTrue false "The updated document should have failed to parse"
@ -395,14 +481,15 @@ let integrationTests =
do! loadDocs () do! loadDocs ()
// This not raising an exception is the test // This not raising an exception is the test
do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Nothing" ] do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.Equal "NumValue" 17 ] [ "Nothing" ]
} }
testTask "succeeds when no document is matched" { testTask "succeeds when no document is matched" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
// This not raising an exception is the test // This not raising an exception is the test
do! conn.removeFieldsByField SqliteDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] do! conn.removeFieldsByFields
SqliteDb.TableName Any [ Field.NotEqual "Abracadabra" "apple" ] [ "Value" ]
} }
] ]
testList "deleteById" [ testList "deleteById" [
@ -425,13 +512,13 @@ let integrationTests =
Expect.equal remaining 5L "There should have been 5 documents remaining" Expect.equal remaining 5L "There should have been 5 documents remaining"
} }
] ]
testList "deleteByField" [ testList "deleteByFields" [
testTask "succeeds when documents are deleted" { testTask "succeeds when documents are deleted" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
do! conn.deleteByField SqliteDb.TableName (Field.NE "Value" "purple") do! conn.deleteByFields SqliteDb.TableName Any [ Field.NotEqual "Value" "purple" ]
let! remaining = conn.countAll SqliteDb.TableName let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 2L "There should have been 2 documents remaining" Expect.equal remaining 2L "There should have been 2 documents remaining"
} }
@ -440,7 +527,7 @@ let integrationTests =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
do! conn.deleteByField SqliteDb.TableName (Field.EQ "Value" "crimson") do! conn.deleteByFields SqliteDb.TableName Any [ Field.Equal "Value" "crimson" ]
let! remaining = conn.countAll SqliteDb.TableName let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining" Expect.equal remaining 5L "There should have been 5 documents remaining"
} }
@ -478,8 +565,8 @@ let integrationTests =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
do! loadDocs () do! loadDocs ()
let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData<JsonDocument> let! docs = conn.customList (Query.find SqliteDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" Expect.hasLength docs 5 "There should have been 5 documents returned"
} }
testTask "succeeds when data is not found" { testTask "succeeds when data is not found" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,39 @@
module Types module Types
type NumIdDocument =
{ Key: int
Text: string }
type SubDocument = type SubDocument =
{ Foo: string { Foo: string
Bar: 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 = type JsonDocument =
{ Id: string { Id: string
Value: string Value: string
NumValue: int NumValue: int
Sub: SubDocument option } Sub: SubDocument option }
/// An empty JsonDocument /// An empty JsonDocument
let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None }
/// Documents to use for testing /// Documents to use for testing
let testDocuments = [ let testDocuments =
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } [ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None } { Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } } { Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None } { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } ]
]

View File

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