This encompasses:
- New behavior for SQLite
- Migrated behavior for PostrgeSQL (from BitBadger.Npgsql.FSharp.Documents)
- New "byField" behavior for PostgreSQL
- A unification of C# and F# centric implementations
This commit is contained in:
Daniel J. Summers 2024-01-06 15:51:48 -05:00 committed by GitHub
parent a0a4f6604c
commit 68ad874256
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 8604 additions and 0 deletions

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# BitBadger.Documents
This library provides a lightweight document storage implementation backed by either PostgreSQL or SQLite. Both of these databases have great support for storing, retrieving, and manipulating JSON fields; this library leverages that, and provides a straightforward way to store documents.
## NuGet Packages
| PostgreSQL | SQLite |
|------------|--------|
|[![Nuget](https://img.shields.io/nuget/v/BitBadger.Documents.Postgres?style=plastic)](https://www.nuget.org/packages/BitBadger.Documents.Postgres/)|[![Nuget](https://img.shields.io/nuget/v/BitBadger.Documents.Sqlite?style=plastic)](https://www.nuget.org/packages/BitBadger.Documents.Sqlite/)|
## More Information
See [the project site](https://bitbadger.solutions/open-source/relational-documents/) for a full description and documentation.

View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/contentModel.xml
/modules.xml
/.idea.BitBadger.Documents.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1 @@
BitBadger.Documents

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders>
<Path>../../BitBadger.Documents</Path>
</attachedFolders>
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,46 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Common", "Common\BitBadger.Documents.Common.fsproj", "{E52D624A-2A1F-4D38-82B6-115907D9CB1A}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Tests", "Tests\BitBadger.Documents.Tests.fsproj", "{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite", "Sqlite\BitBadger.Documents.Sqlite.fsproj", "{B8A82483-1E72-46D2-B29A-1C371AC5DD20}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.CSharp", "Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj", "{AB58418C-7F90-467E-8F67-F4E0AD9D8875}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Postgres", "Postgres\BitBadger.Documents.Postgres.fsproj", "{30E73486-9D00-440B-B4AC-5B7AC029AE72}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.Build.0 = Release|Any CPU
{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Release|Any CPU.Build.0 = Release|Any CPU
{B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.Build.0 = Release|Any CPU
{AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.Build.0 = Release|Any CPU
{30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30E73486-9D00-440B-B4AC-5B7AC029AE72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30E73486-9D00-440B-B4AC-5B7AC029AE72}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Initial release (RC 1)</PackageReleaseNotes>
<PackageTags>JSON Document SQL</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.2.42" />
</ItemGroup>
</Project>

221
src/Common/Library.fs Normal file
View File

@ -0,0 +1,221 @@
namespace BitBadger.Documents
/// The types of logical operations available for JSON fields
[<Struct>]
type Op =
/// Equals (=)
| EQ
/// Greater Than (>)
| GT
/// Greater Than or Equal To (>=)
| GE
/// Less Than (<)
| LT
/// Less Than or Equal To (<=)
| LE
/// Not Equal to (<>)
| NE
/// Exists (IS NOT NULL)
| EX
/// Does Not Exist (IS NULL)
| NEX
override this.ToString() =
match this with
| EQ -> "="
| GT -> ">"
| GE -> ">="
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
/// The required document serialization implementation
type IDocumentSerializer =
/// Serialize an object to a JSON string
abstract Serialize<'T> : 'T -> string
/// Deserialize a JSON string into an object
abstract Deserialize<'T> : string -> 'T
/// Document serializer defaults
module DocumentSerializer =
open System.Text.Json
open System.Text.Json.Serialization
/// The default JSON serializer options to use with the stock serializer
let private jsonDefaultOpts =
let o = JsonSerializerOptions()
o.Converters.Add(JsonFSharpConverter())
o
/// The default JSON serializer
[<CompiledName "Default">]
let ``default`` =
{ new IDocumentSerializer with
member _.Serialize<'T>(it: 'T) : string =
JsonSerializer.Serialize(it, jsonDefaultOpts)
member _.Deserialize<'T>(it: string) : 'T =
JsonSerializer.Deserialize<'T>(it, jsonDefaultOpts)
}
/// Configuration for document handling
[<RequireQualifiedAccess>]
module Configuration =
/// The serializer to use for document manipulation
let mutable private serializerValue = DocumentSerializer.``default``
/// Register a serializer to use for translating documents to domain types
[<CompiledName "UseSerializer">]
let useSerializer ser =
serializerValue <- ser
/// Retrieve the currently configured serializer
[<CompiledName "Serializer">]
let serializer () =
serializerValue
/// The serialized name of the ID field for documents
let mutable idFieldValue = "Id"
/// Specify the name of the ID field for documents
[<CompiledName "UseIdField">]
let useIdField it =
idFieldValue <- it
/// Retrieve the currently configured ID field for documents
[<CompiledName "IdField">]
let idField () =
idFieldValue
/// Query construction functions
[<RequireQualifiedAccess>]
module Query =
/// Create a SELECT clause to retrieve the document data from the given table
[<CompiledName "SelectFromTable">]
let selectFromTable tableName =
$"SELECT data FROM %s{tableName}"
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
let whereByField fieldName op paramName =
let theRest =
match op with
| EX | NEX -> string op
| _ -> $"{op} %s{paramName}"
$"data ->> '%s{fieldName}' {theRest}"
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Configuration.idField ()) EQ paramName
/// Queries to define tables and indexes
module Definition =
/// SQL statement to create a document table
[<CompiledName "EnsureTableFor">]
let ensureTableFor name dataType =
$"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)"
/// Split a schema and table name
let private splitSchemaAndTable (tableName: string) =
let parts = tableName.Split '.'
if Array.length parts = 1 then "", tableName else parts[0], parts[1]
/// SQL statement to create an index on one or more fields in a JSON document
[<CompiledName "EnsureIndexOn">]
let ensureIndexOn tableName indexName (fields: string seq) =
let _, tbl = splitSchemaAndTable tableName
let jsonFields =
fields
|> Seq.map (fun it ->
let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"(data ->> '{fieldName}'){direction}")
|> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
/// SQL statement to create a key index for a document table
[<CompiledName "EnsureKey">]
let ensureKey tableName =
(ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX")
/// Query to insert a document
[<CompiledName "Insert">]
let insert tableName =
$"INSERT INTO %s{tableName} VALUES (@data)"
/// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]
let save tableName =
sprintf
"INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ())
/// Query to 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 fieldName op =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField fieldName op "@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 fieldName op =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField fieldName op "@field"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op =
$"""{selectFromTable tableName} WHERE {whereByField fieldName op "@field"}"""
/// 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 fieldName op =
$"""DELETE FROM %s{tableName} WHERE {whereByField fieldName op "@field"}"""

17
src/Common/README.md Normal file
View File

@ -0,0 +1,17 @@
# BitBadger.Documents.Common
This package provides common definitions and functionality for `BitBadger.Documents` implementations. These libraries provide a document storage view over relational databases, while also providing convenience functions for relational usage as well. This enables a hybrid approach to data storage, allowing the user to use documents where they make sense, while streamlining traditional ADO.NET functionality where relational data is required.
- `BitBadger.Documents.Postgres` ([NuGet](https://www.nuget.org/packages/BitBadger.Documents.Postgres/)) provides a PostgreSQL implementation.
- `BitBadger.Documents.Sqlite` ([NuGet](https://www.nuget.org/packages/BitBadger.Documents.Sqlite/)) provides a SQLite implementation
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries)
- Accesses documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Uses `Task`-based async for all data access functions
- Uses building blocks for more complex queries
## Getting Started
Install the library of your choice and follow its README; also, the [project site](https://bitbadger.solutions/open-source/relational-documents/) has complete documentation.

21
src/Directory.Build.props Normal file
View File

@ -0,0 +1,21 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionSuffix>rc-1</VersionSuffix>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://bitbadger.solutions/open-source/relational-documents/</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/bit-badger/BitBadger.Documents</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Initial release; migrated from BitBadger.Npgsql.Documents, with field and extension additions (RC 1)</PackageReleaseNotes>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<Compile Include="Extensions.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
</ItemGroup>
</Project>

333
src/Postgres/Extensions.fs Normal file
View File

@ -0,0 +1,333 @@
namespace BitBadger.Documents.Postgres
open Npgsql
open Npgsql.FSharp
/// F# Extensions for the NpgsqlConnection type
[<AutoOpen>]
module Extensions =
type NpgsqlConnection with
/// Execute a query that returns a list of results
member conn.customList<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.list<'TDoc> query parameters mapFunc (Sql.existingConnection conn)
/// Execute a query that returns one or no results; returns None if not found
member conn.customSingle<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.single<'TDoc> query parameters mapFunc (Sql.existingConnection conn)
/// Execute a query that returns no results
member conn.customNonQuery query parameters =
WithProps.Custom.nonQuery query parameters (Sql.existingConnection conn)
/// Execute a query that returns a scalar value
member conn.customScalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) =
WithProps.Custom.scalar query parameters mapFunc (Sql.existingConnection conn)
/// Create a document table
member conn.ensureTable name =
WithProps.Definition.ensureTable name (Sql.existingConnection conn)
/// Create an index on documents in the specified table
member conn.ensureDocumentIndex name idxType =
WithProps.Definition.ensureDocumentIndex name idxType (Sql.existingConnection conn)
/// Create an index on field(s) within documents in the specified table
member conn.ensureFieldIndex tableName indexName fields =
WithProps.Definition.ensureFieldIndex tableName indexName fields (Sql.existingConnection conn)
/// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.insert<'TDoc> tableName document (Sql.existingConnection conn)
/// 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) =
WithProps.Document.save<'TDoc> tableName document (Sql.existingConnection conn)
/// Count all documents in a table
member conn.countAll tableName =
WithProps.Count.all tableName (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =)
member conn.countByField tableName fieldName op (value: obj) =
WithProps.Count.byField tableName fieldName op value (Sql.existingConnection conn)
/// Count matching documents using a JSON containment query (@>)
member conn.countByContains tableName criteria =
WithProps.Count.byContains tableName criteria (Sql.existingConnection conn)
/// Count matching documents using a JSON Path match query (@?)
member conn.countByJsonPath tableName jsonPath =
WithProps.Count.byJsonPath tableName jsonPath (Sql.existingConnection conn)
/// Determine if a document exists for the given ID
member conn.existsById tableName docId =
WithProps.Exists.byId tableName docId (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
member conn.existsByField tableName fieldName op (value: obj) =
WithProps.Exists.byField tableName fieldName op value (Sql.existingConnection conn)
/// Determine if documents exist using a JSON containment query (@>)
member conn.existsByContains tableName criteria =
WithProps.Exists.byContains tableName criteria (Sql.existingConnection conn)
/// Determine if documents exist using a JSON Path match query (@?)
member conn.existsByJsonPath tableName jsonPath =
WithProps.Exists.byJsonPath tableName jsonPath (Sql.existingConnection conn)
/// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName =
WithProps.Find.all<'TDoc> tableName (Sql.existingConnection conn)
/// Retrieve a document by its ID; returns None if not found
member conn.findById<'TKey, 'TDoc> tableName docId =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
member conn.findByField<'TDoc> tableName fieldName op (value: obj) =
WithProps.Find.byField<'TDoc> tableName fieldName op value (Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>)
member conn.findByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn)
/// Retrieve documents matching a JSON Path match query (@?)
member conn.findByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) =
WithProps.Find.firstByField<'TDoc> tableName fieldName op value (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
member conn.findFirstByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
member conn.findFirstByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn)
/// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
WithProps.Update.byId tableName docId document (Sql.existingConnection conn)
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithProps.Update.byFunc tableName idFunc document (Sql.existingConnection conn)
/// Patch a document by its ID
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
member conn.patchByField tableName fieldName op (value: obj) (patch: 'TPatch) =
WithProps.Patch.byField tableName fieldName op value patch (Sql.existingConnection conn)
/// Patch documents using a JSON containment query in the WHERE clause (@>)
member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
WithProps.Patch.byContains tableName criteria patch (Sql.existingConnection conn)
/// Patch documents using a JSON Path match query in the WHERE clause (@?)
member conn.patchByJsonPath tableName jsonPath (patch: 'TPatch) =
WithProps.Patch.byJsonPath tableName jsonPath patch (Sql.existingConnection conn)
/// Delete a document by its ID
member conn.deleteById tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
member conn.deleteByField tableName fieldName op (value: obj) =
WithProps.Delete.byField tableName fieldName op value (Sql.existingConnection conn)
/// Delete documents by matching a JSON containment query (@>)
member conn.deleteByContains tableName (criteria: 'TContains) =
WithProps.Delete.byContains tableName criteria (Sql.existingConnection conn)
/// Delete documents by matching a JSON Path match query (@?)
member conn.deleteByJsonPath tableName path =
WithProps.Delete.byJsonPath tableName path (Sql.existingConnection conn)
open System.Runtime.CompilerServices
/// C# extensions on the NpgsqlConnection type
type NpgsqlConnectionCSharpExtensions =
/// Execute a query that returns a list of results
[<Extension>]
static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn)
/// Execute a query that returns one or no results; returns None if not found
[<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null>(
conn, query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn)
/// Execute a query that returns no results
[<Extension>]
static member inline CustomNonQuery(conn, query, parameters) =
WithProps.Custom.nonQuery query parameters (Sql.existingConnection conn)
/// Execute a query that returns a scalar value
[<Extension>]
static member inline CustomScalar<'T when 'T: struct>(
conn, query, parameters, mapFunc: System.Func<RowReader, 'T>) =
WithProps.Custom.Scalar(query, parameters, mapFunc, Sql.existingConnection conn)
/// Create a document table
[<Extension>]
static member inline EnsureTable(conn, name) =
WithProps.Definition.ensureTable name (Sql.existingConnection conn)
/// Create an index on documents in the specified table
[<Extension>]
static member inline EnsureDocumentIndex(conn, name, idxType) =
WithProps.Definition.ensureDocumentIndex name idxType (Sql.existingConnection conn)
/// Create an index on field(s) within documents in the specified table
[<Extension>]
static member inline EnsureFieldIndex(conn, tableName, indexName, fields) =
WithProps.Definition.ensureFieldIndex tableName indexName fields (Sql.existingConnection conn)
/// Insert a new document
[<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
WithProps.Document.insert<'TDoc> tableName document (Sql.existingConnection conn)
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
WithProps.Document.save<'TDoc> tableName document (Sql.existingConnection conn)
/// Count all documents in a table
[<Extension>]
static member inline CountAll(conn, tableName) =
WithProps.Count.all tableName (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
static member inline CountByField(conn, tableName, fieldName, op, value: obj) =
WithProps.Count.byField tableName fieldName op value (Sql.existingConnection conn)
/// Count matching documents using a JSON containment query (@>)
[<Extension>]
static member inline CountByContains(conn, tableName, criteria: 'TCriteria) =
WithProps.Count.byContains tableName criteria (Sql.existingConnection conn)
/// Count matching documents using a JSON Path match query (@?)
[<Extension>]
static member inline CountByJsonPath(conn, tableName, jsonPath) =
WithProps.Count.byJsonPath tableName jsonPath (Sql.existingConnection conn)
/// Determine if a document exists for the given ID
[<Extension>]
static member inline ExistsById(conn, tableName, docId) =
WithProps.Exists.byId tableName docId (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) =
WithProps.Exists.byField tableName fieldName op value (Sql.existingConnection conn)
/// Determine if documents exist using a JSON containment query (@>)
[<Extension>]
static member inline ExistsByContains(conn, tableName, criteria: 'TCriteria) =
WithProps.Exists.byContains tableName criteria (Sql.existingConnection conn)
/// Determine if documents exist using a JSON Path match query (@?)
[<Extension>]
static member inline ExistsByJsonPath(conn, tableName, jsonPath) =
WithProps.Exists.byJsonPath tableName jsonPath (Sql.existingConnection conn)
/// Retrieve all documents in the given table
[<Extension>]
static member inline FindAll<'TDoc>(conn, tableName) =
WithProps.Find.All<'TDoc>(tableName, Sql.existingConnection conn)
/// Retrieve a document by its ID; returns None if not found
[<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value: obj) =
WithProps.Find.ByField<'TDoc>(tableName, fieldName, op, value, Sql.existingConnection conn)
/// Retrieve documents matching a JSON containment query (@>)
[<Extension>]
static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) =
WithProps.Find.ByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn)
/// Retrieve documents matching a JSON Path match query (@?)
[<Extension>]
static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) =
WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<Extension>]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) =
WithProps.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<Extension>]
static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
[<Extension>]
static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn)
/// Update an entire document by its ID
[<Extension>]
static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) =
WithProps.Update.byId tableName docId document (Sql.existingConnection conn)
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<Extension>]
static member inline UpdateByFunc(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
WithProps.Update.ByFunc(tableName, idFunc, document, Sql.existingConnection conn)
/// Patch a document by its ID
[<Extension>]
static member inline PatchById(conn, tableName, docId: 'TKey, patch: 'TPatch) =
WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
static member inline PatchByField(conn, tableName, fieldName, op, value: obj, patch: 'TPatch) =
WithProps.Patch.byField tableName fieldName op value patch (Sql.existingConnection conn)
/// Patch documents using a JSON containment query in the WHERE clause (@>)
[<Extension>]
static member inline PatchByContains(conn, tableName, criteria: 'TCriteria, patch: 'TPatch) =
WithProps.Patch.byContains tableName criteria patch (Sql.existingConnection conn)
/// Patch documents using a JSON Path match query in the WHERE clause (@?)
[<Extension>]
static member inline PatchByJsonPath(conn, tableName, jsonPath, patch: 'TPatch) =
WithProps.Patch.byJsonPath tableName jsonPath patch (Sql.existingConnection conn)
/// Delete a document by its ID
[<Extension>]
static member inline DeleteById(conn, tableName, docId: 'TKey) =
WithProps.Delete.byId tableName docId (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
static member inline DeleteByField(conn, tableName, fieldName, op, value: obj) =
WithProps.Delete.byField tableName fieldName op value (Sql.existingConnection conn)
/// Delete documents by matching a JSON containment query (@>)
[<Extension>]
static member inline DeleteByContains(conn, tableName, criteria: 'TContains) =
WithProps.Delete.byContains tableName criteria (Sql.existingConnection conn)
/// Delete documents by matching a JSON Path match query (@?)
[<Extension>]
static member inline DeleteByJsonPath(conn, tableName, path) =
WithProps.Delete.byJsonPath tableName path (Sql.existingConnection conn)

770
src/Postgres/Library.fs Normal file
View File

@ -0,0 +1,770 @@
namespace BitBadger.Documents.Postgres
/// The type of index to generate for the document
[<Struct>]
type DocumentIndex =
/// A GIN index with standard operations (all operators supported)
| Full
/// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators)
| Optimized
open Npgsql
/// Configuration for document handling
module Configuration =
/// The data source to use for query execution
let mutable private dataSourceValue : NpgsqlDataSource option = None
/// Register a data source to use for query execution (disposes the current one if it exists)
[<CompiledName "UseDataSource">]
let useDataSource source =
if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose()
dataSourceValue <- Some source
/// Retrieve the currently configured data source
[<CompiledName "DataSource">]
let dataSource () =
match dataSourceValue with
| Some source -> source
| None -> invalidOp "Please provide a data source before attempting data access"
open Npgsql.FSharp
/// Helper functions
[<AutoOpen>]
module private Helpers =
/// Shorthand to retrieve the data source as SqlProps
let internal fromDataSource () =
Configuration.dataSource () |> Sql.fromDataSource
/// Execute a task and ignore the result
let internal ignoreTask<'T> (it : System.Threading.Tasks.Task<'T>) = backgroundTask {
let! _ = it
()
}
open BitBadger.Documents
/// Functions for creating parameters
[<AutoOpen>]
module Parameters =
/// Create an ID parameter (name "@id", key will be treated as a string)
[<CompiledName "Id">]
let idParam (key: 'TKey) =
"@id", Sql.string (string key)
/// Create a parameter with a JSON value
[<CompiledName "Json">]
let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field")
[<CompiledName "Field">]
let fieldParam (value: obj) =
"@field", Sql.parameter (NpgsqlParameter("@field", value))
/// An empty parameter sequence
[<CompiledName "None">]
let noParams =
Seq.empty<string * SqlValue>
/// Query construction functions
[<RequireQualifiedAccess>]
module Query =
/// Table and index definition queries
module Definition =
/// SQL statement to create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
Query.Definition.ensureTableFor name "JSONB"
/// SQL statement to create an index on JSON documents in the specified table
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex (name: string) idxType =
let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops"
let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition
[<CompiledName "WhereDataContains">]
let whereDataContains paramName =
$"data @> %s{paramName}"
/// Create a WHERE clause fragment to implement a @? (JSON Path match) condition
[<CompiledName "WhereJsonPathMatches">]
let whereJsonPathMatches paramName =
$"data @? %s{paramName}::jsonpath"
/// Queries for counting documents
module Count =
/// Query to count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}"""
/// Query to count matching documents using a JSON Path match (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}"""
/// Queries for determining document existence
module Exists =
/// Query to determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it"""
/// Query to determine if documents exist using a JSON Path match (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
$"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}"""
/// Query to retrieve documents using a JSON Path match (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
$"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}"""
/// Queries to patch (partially update) documents
module Patch =
/// Query to patch a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}"""
/// Query to patch documents match a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op =
$"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}"""
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
$"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}"""
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
$"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}"""
/// Queries to delete documents
module Delete =
/// Query to delete documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
$"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}"""
/// Query to delete documents using a JSON Path match (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
$"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}"""
/// Functions for dealing with results
[<AutoOpen>]
module Results =
/// Create a domain item from a document, specifying the field in which the document is found
[<CompiledName "FromDocument">]
let fromDocument<'T> field (row: RowReader) : 'T =
Configuration.serializer().Deserialize<'T>(row.string field)
/// Create a domain item from a document
[<CompiledName "FromData">]
let fromData<'T> row : 'T =
fromDocument "data" row
/// Extract a count from the column "it"
[<CompiledName "ToCount">]
let toCount (row: RowReader) =
row.int "it"
/// Extract a true/false value from the column "it"
[<CompiledName "ToExists">]
let toExists (row: RowReader) =
row.bool "it"
/// Versions of queries that accept SqlProps as the last parameter
module WithProps =
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps =
Sql.query query sqlProps
|> Sql.parameters parameters
|> Sql.executeAsync mapFunc
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps
return ResizeArray results
}
/// Execute a query that returns one or no results; returns None if not found
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters mapFunc sqlProps = backgroundTask {
let! results = list<'TDoc> query parameters mapFunc sqlProps
return FSharp.Collections.List.tryHead results
}
/// Execute a query that returns one or no results; returns null if not found
let Single<'TDoc when 'TDoc: null>(
query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! result = single<'TDoc> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
return Option.toObj result
}
/// Execute a query that returns no results
[<CompiledName "NonQuery">]
let nonQuery query parameters sqlProps =
Sql.query query sqlProps
|> Sql.parameters (FSharp.Collections.List.ofSeq parameters)
|> Sql.executeNonQueryAsync
|> ignoreTask
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps =
Sql.query query sqlProps
|> Sql.parameters parameters
|> Sql.executeRowAsync mapFunc
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) =
scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
/// Table and index definition commands
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name sqlProps = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps
do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps
}
/// Create an index on documents in the specified table
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex name idxType sqlProps =
Custom.nonQuery (Query.Definition.ensureDocumentIndex name idxType) [] sqlProps
/// Create an index on field(s) within documents in the specified table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields sqlProps =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] sqlProps
/// Commands to add documents
[<AutoOpen>]
module Document =
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) sqlProps =
Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps
/// 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) sqlProps =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] sqlProps
/// Commands to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName sqlProps =
Custom.scalar (Query.Count.all tableName) [] toCount sqlProps
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) sqlProps =
Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] toCount sqlProps
/// Count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) sqlProps =
Custom.scalar (Query.Count.byContains tableName) [ jsonParam "@criteria" criteria ] toCount sqlProps
/// Count matching documents using a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath sqlProps =
Custom.scalar (Query.Count.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toCount sqlProps
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) sqlProps =
Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) sqlProps =
Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] toExists sqlProps
/// Determine if a document exists using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) sqlProps =
Custom.scalar (Query.Exists.byContains tableName) [ jsonParam "@criteria" criteria ] toExists sqlProps
/// Determine if a document exists using a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath sqlProps =
Custom.scalar (Query.Exists.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toExists sqlProps
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName sqlProps =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps
/// Retrieve all documents in the given table
let All<'TDoc>(tableName, sqlProps) =
Custom.List<'TDoc>(Query.selectFromTable tableName, [], fromData<'TDoc>, sqlProps)
/// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps =
Custom.single (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> sqlProps
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) =
Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, sqlProps)
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName fieldName op (value: obj) sqlProps =
Custom.list<'TDoc> (Query.Find.byField tableName fieldName op) [ fieldParam value ] fromData<'TDoc> sqlProps
/// Retrieve documents matching a JSON field comparison (->> =)
let ByField<'TDoc>(tableName, fieldName, op, value: obj, sqlProps) =
Custom.List<'TDoc>(
Query.Find.byField tableName fieldName op, [ fieldParam value ], fromData<'TDoc>, sqlProps)
/// Retrieve documents matching a JSON containment query (@>)
[<CompiledName "FSharpByContains">]
let byContains<'TDoc> tableName (criteria: obj) sqlProps =
Custom.list<'TDoc>
(Query.Find.byContains tableName) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps
/// Retrieve documents matching a JSON containment query (@>)
let ByContains<'TDoc>(tableName, criteria: obj, sqlProps) =
Custom.List<'TDoc>(
Query.Find.byContains tableName, [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps)
/// Retrieve documents matching a JSON Path match query (@?)
[<CompiledName "FSharpByJsonPath">]
let byJsonPath<'TDoc> tableName jsonPath sqlProps =
Custom.list<'TDoc>
(Query.Find.byJsonPath tableName) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps
/// Retrieve documents matching a JSON Path match query (@?)
let ByJsonPath<'TDoc>(tableName, jsonPath, sqlProps) =
Custom.List<'TDoc>(
Query.Find.byJsonPath tableName, [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName fieldName op (value: obj) sqlProps =
Custom.single<'TDoc>
$"{Query.Find.byField tableName fieldName op} LIMIT 1" [ fieldParam value ] fromData<'TDoc> sqlProps
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.Find.byField tableName fieldName op} LIMIT 1", [ fieldParam value ], fromData<'TDoc>, sqlProps)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<CompiledName "FSharpFirstByContains">]
let firstByContains<'TDoc> tableName (criteria: obj) sqlProps =
Custom.single<'TDoc>
$"{Query.Find.byContains tableName} LIMIT 1" [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps
/// Retrieve the first document matching a JSON containment query (@>); returns null if not found
let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.Find.byContains tableName} LIMIT 1",
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath sqlProps =
Custom.single<'TDoc>
$"{Query.Find.byJsonPath tableName} LIMIT 1" [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps
/// Retrieve the first document matching a JSON Path match query (@?); returns null if not found
let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.Find.byJsonPath tableName} LIMIT 1",
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) sqlProps =
Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] sqlProps
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps =
byId tableName (idFunc document) document sqlProps
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) =
byFunc tableName idFunc.Invoke document sqlProps
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) sqlProps =
Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] sqlProps
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.Patch.byField tableName fieldName op) [ jsonParam "@data" patch; fieldParam value ] sqlProps
/// Patch documents using a JSON containment query in the WHERE clause (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.Patch.byContains tableName) [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] sqlProps
/// Patch documents using a JSON Path match query in the WHERE clause (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.Patch.byJsonPath tableName) [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] sqlProps
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) sqlProps =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) sqlProps =
Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] sqlProps
/// Delete documents by matching a JSON contains query (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TCriteria) sqlProps =
Custom.nonQuery (Query.Delete.byContains tableName) [ jsonParam "@criteria" criteria ] sqlProps
/// Delete documents by matching a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName path sqlProps =
Custom.nonQuery (Query.Delete.byJsonPath tableName) [ "@path", Sql.string path ] sqlProps
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.list<'TDoc> query parameters mapFunc (fromDataSource ())
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// Execute a query that returns one or no results; returns None if not found
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ())
/// Execute a query that returns one or no results; returns null if not found
let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// Execute a query that returns no results
[<CompiledName "NonQuery">]
let nonQuery query parameters =
WithProps.Custom.nonQuery query parameters (fromDataSource ())
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) =
WithProps.Custom.scalar query parameters mapFunc (fromDataSource ())
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>) =
WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ())
/// Table and index definition commands
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
WithProps.Definition.ensureTable name (fromDataSource ())
/// Create an index on documents in the specified table
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex name idxType =
WithProps.Definition.ensureDocumentIndex name idxType (fromDataSource ())
/// Create an index on field(s) within documents in the specified table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
WithProps.Definition.ensureFieldIndex tableName indexName fields (fromDataSource ())
/// Document writing functions
[<AutoOpen>]
module Document =
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.insert<'TDoc> tableName document (fromDataSource ())
/// 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) =
WithProps.Document.save<'TDoc> tableName document (fromDataSource ())
/// Queries to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName =
WithProps.Count.all tableName (fromDataSource ())
/// Count matching documents using a JSON field comparison query (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
WithProps.Count.byField tableName fieldName op value (fromDataSource ())
/// Count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Count.byContains tableName criteria (fromDataSource ())
/// Count matching documents using a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ())
/// Queries to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName docId =
WithProps.Exists.byId tableName docId (fromDataSource ())
/// Determine if documents exist using a JSON field comparison query (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
WithProps.Exists.byField tableName fieldName op value (fromDataSource ())
/// Determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Exists.byContains tableName criteria (fromDataSource ())
/// Determine if documents exist using a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ())
/// Commands to retrieve documents
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
WithProps.Find.all<'TDoc> tableName (fromDataSource ())
/// Retrieve all documents in the given table
let All<'TDoc> tableName =
WithProps.Find.All<'TDoc>(tableName, fromDataSource ())
/// Retrieve a document by its ID; returns None if not found
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ())
/// Retrieve a document by its ID; returns null if not found
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =)
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName fieldName op (value: obj) =
WithProps.Find.byField<'TDoc> tableName fieldName op value (fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =)
let ByField<'TDoc>(tableName, fieldName, op, value: obj) =
WithProps.Find.ByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ())
/// Retrieve documents matching a JSON containment query (@>)
[<CompiledName "FSharpByContains">]
let byContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.byContains<'TDoc> tableName criteria (fromDataSource ())
/// Retrieve documents matching a JSON containment query (@>)
let ByContains<'TDoc>(tableName, criteria: obj) =
WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// Retrieve documents matching a JSON Path match query (@?)
[<CompiledName "FSharpByJsonPath">]
let byJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// Retrieve documents matching a JSON Path match query (@?)
let ByJsonPath<'TDoc>(tableName, jsonPath) =
WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName fieldName op (value: obj) =
WithProps.Find.firstByField<'TDoc> tableName fieldName op value (fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj) =
WithProps.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ())
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<CompiledName "FSharpFirstByContains">]
let firstByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ())
/// Retrieve the first document matching a JSON containment query (@>); returns null if not found
let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// Retrieve the first document matching a JSON Path match query (@?); returns null if not found
let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
WithProps.Update.byId tableName docId document (fromDataSource ())
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpFullFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithProps.Update.byFunc tableName idFunc document (fromDataSource ())
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
WithProps.Update.ByFunc(tableName, idFunc, document, fromDataSource ())
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
WithProps.Patch.byId tableName docId patch (fromDataSource ())
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) (patch: 'TPatch) =
WithProps.Patch.byField tableName fieldName op value patch (fromDataSource ())
/// Patch documents using a JSON containment query in the WHERE clause (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
WithProps.Patch.byContains tableName criteria patch (fromDataSource ())
/// Patch documents using a JSON Path match query in the WHERE clause (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath (patch: 'TPatch) =
WithProps.Patch.byJsonPath tableName jsonPath patch (fromDataSource ())
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (fromDataSource ())
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
WithProps.Delete.byField tableName fieldName op value (fromDataSource ())
/// Delete documents by matching a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) =
WithProps.Delete.byContains tableName criteria (fromDataSource ())
/// Delete documents by matching a JSON Path match query (@?)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName path =
WithProps.Delete.byJsonPath tableName path (fromDataSource ())

101
src/Postgres/README.md Normal file
View File

@ -0,0 +1,101 @@
# BitBadger.Documents.Postgres
This package provides a lightweight document library backed by [PostgreSQL](https://www.postgresql.org). It also provides streamlined functions for traditional ADO.NET functionality where relational data is required. Both C# and F# have first-class implementations.
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries)
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions
- Use building blocks for more complex queries
## Getting Started
Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library:
```csharp
// C#
using BitBadger.Documents.Postgres;
//...
// Do not use "using" here; the library will handle disposing this instance
var data = new NpgsqlDataSourceBuilder("connection-string").Build();
Postgres.Configuration.UseDataSource(data);
```
```fsharp
// F#
open BitBadger.Documents.Postgres
// ...
// Do not use "use" here; the library will handle disposing this instance
let dataSource = // same as above ....
Configuration.useDataSource dataSource
// ...
```
By default, the library uses a `System.Text.Json`-based serializer configured to use the `FSharp.SystemTextJson` converter. To provide a different serializer (different options, more converters, etc.), construct it to implement `IDocumentSerializer` and provide it via `Configuration.useSerializer`. If custom serialization makes the serialized Id field not be `Id`, that will also need to be configured.
## Using
Retrieve all customers:
```csharp
// C#; parameter is table name
// Find.All type signature is Func<string, Task<List<TDoc>>>
var customers = await Find.All("customer");
```
```fsharp
// F#
// Find.all type signature is string -> Task<'TDoc list>
let! customers = Find.all<Customer> "customer"
```
Select a customer by ID:
```csharp
// C#; parameters are table name and ID
// Find.ById type signature is Func<string, TKey, Task<TDoc?>>
var customer = await Find.ById<string, Customer>("customer", "123");
```
```fsharp
// F#
// Find.byId type signature is string -> 'TKey -> Task<'TDoc option>
let! customer = Find.byId<string, Customer> "customer" "123"
```
_(keys are treated as strings in the database)_
Count customers in Atlanta (using JSON containment):
```csharp
// C#; parameters are table name and object for containment query
// Count.ByContains type signature is Func<string, TCriteria, int>
var customerCount = await Count.ByContains("customer", new { City = "Atlanta" });
```
```fsharp
// F#
// Count.byContains type signature is string -> 'TCriteria -> Task<int>
let! customerCount = Count.byContains "customer" {| City = "Atlanta" |}
```
Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp
// C#; parameters are table name and JSON Path expression
// Delete.ByJsonPath type signature is Func<string, string, Task>
await Delete.ByJsonPath("customer", "$.City ? (@ == \"Chicago\")");
```
```fsharp
// F#
// Delete.byJsonPath type signature is string -> string -> Task<unit>
do! Delete.byJsonPath "customer" """$.City ? (@ == "Chicago")"""
```
## More Information
The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library.

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Initial release; SQLite document implementation similar to BitBadger.Npgsql.Documents (RC 1)</PackageReleaseNotes>
<PackageTags>JSON Document SQLite</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<Compile Include="Extensions.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>BitBadger.Documents.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>BitBadger.Documents.Tests.CSharp</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
</ItemGroup>
</Project>

215
src/Sqlite/Extensions.fs Normal file
View File

@ -0,0 +1,215 @@
namespace BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
/// F# extensions for the SqliteConnection type
[<AutoOpen>]
module Extensions =
type SqliteConnection with
/// Execute a query that returns a list of results
member conn.customList<'TDoc> query parameters mapFunc =
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// Execute a query that returns one or no results
member conn.customSingle<'TDoc> query parameters mapFunc =
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// Execute a query that does not return a value
member conn.customNonQuery query parameters =
WithConn.Custom.nonQuery query parameters conn
/// Execute a query that returns a scalar value
member conn.customScalar<'T when 'T: struct> query parameters mapFunc =
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// Create a document table
member conn.ensureTable name =
WithConn.Definition.ensureTable name conn
/// Create an index on a document table
member conn.ensureFieldIndex tableName indexName fields =
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) =
WithConn.insert<'TDoc> tableName document conn
/// 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) =
WithConn.save tableName document conn
/// Count all documents in a table
member conn.countAll tableName =
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field
member conn.countByField tableName fieldName op (value: obj) =
WithConn.Count.byField tableName fieldName op value conn
/// Determine if a document exists for the given ID
member conn.existsById tableName (docId: 'TKey) =
WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field
member conn.existsByField tableName fieldName op (value: obj) =
WithConn.Exists.byField tableName fieldName op value conn
/// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName =
WithConn.Find.all<'TDoc> tableName conn
/// Retrieve a document by its ID
member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) =
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve documents via a comparison on a JSON field
member conn.findByField<'TDoc> tableName fieldName op (value: obj) =
WithConn.Find.byField<'TDoc> tableName fieldName op value conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) =
WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn
/// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
WithConn.Update.byId tableName docId document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithConn.Update.byFunc tableName idFunc document conn
/// Patch a document by its ID
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field
member conn.patchByField tableName fieldName op (value: obj) (patch: 'TPatch) =
WithConn.Patch.byField tableName fieldName op value patch conn
/// Delete a document by its ID
member conn.deleteById tableName (docId: 'TKey) =
WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field
member conn.deleteByField tableName fieldName op (value: obj) =
WithConn.Delete.byField tableName fieldName op value conn
open System.Runtime.CompilerServices
/// C# extensions on the SqliteConnection type
type SqliteConnectionCSharpExtensions =
/// Execute a query that returns a list of results
[<Extension>]
static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that returns one or no results
[<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that does not return a value
[<Extension>]
static member inline CustomNonQuery(conn, query, parameters) =
WithConn.Custom.nonQuery query parameters conn
/// Execute a query that returns a scalar value
[<Extension>]
static member inline CustomScalar<'T when 'T: struct>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// Create a document table
[<Extension>]
static member inline EnsureTable(conn, name) =
WithConn.Definition.ensureTable name conn
/// Create an index on one or more fields in a document table
[<Extension>]
static member inline EnsureFieldIndex(conn, tableName, indexName, fields) =
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// Insert a new document
[<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.insert<'TDoc> tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.save<'TDoc> tableName document conn
/// Count all documents in a table
[<Extension>]
static member inline CountAll(conn, tableName) =
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field
[<Extension>]
static member inline CountByField(conn, tableName, fieldName, op, value: obj) =
WithConn.Count.byField tableName fieldName op value conn
/// Determine if a document exists for the given ID
[<Extension>]
static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field
[<Extension>]
static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) =
WithConn.Exists.byField tableName fieldName op value conn
/// Retrieve all documents in the given table
[<Extension>]
static member inline FindAll<'TDoc>(conn, tableName) =
WithConn.Find.All<'TDoc>(tableName, conn)
/// Retrieve a document by its ID
[<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) =
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on a JSON field
[<Extension>]
static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value) =
WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<Extension>]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) =
WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn)
/// Update an entire document by its ID
[<Extension>]
static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) =
WithConn.Update.byId tableName docId document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<Extension>]
static member inline UpdateByFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) =
WithConn.Update.ByFunc(tableName, idFunc, doc, conn)
/// Patch a document by its ID
[<Extension>]
static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field
[<Extension>]
static member inline PatchByField<'TPatch>(conn, tableName, fieldName, op, value: obj, patch: 'TPatch) =
WithConn.Patch.byField tableName fieldName op value patch conn
/// Delete a document by its ID
[<Extension>]
static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) =
WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field
[<Extension>]
static member inline DeleteByField(conn, tableName, fieldName, op, value: obj) =
WithConn.Delete.byField tableName fieldName op value conn

539
src/Sqlite/Library.fs Normal file
View File

@ -0,0 +1,539 @@
namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite
/// Configuration for document handling
module Configuration =
/// The connection string to use for query execution
let mutable internal connectionString: string option = None
/// Register a connection string to use for query execution (enables foreign keys)
[<CompiledName "UseConnectionString">]
let useConnectionString connStr =
let builder = SqliteConnectionStringBuilder(connStr)
builder.ForeignKeys <- Option.toNullable (Some true)
connectionString <- Some (string builder)
/// Retrieve the currently configured data source
[<CompiledName "DbConn">]
let dbConn () =
match connectionString with
| Some connStr ->
let conn = new SqliteConnection(connStr)
conn.Open()
conn
| None -> invalidOp "Please provide a connection string before attempting data access"
/// Query definitions
[<RequireQualifiedAccess>]
module Query =
/// Data definition
module Definition =
/// SQL statement to create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
/// Document patching (partial update) queries
module Patch =
/// Query to patch (partially update) a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {Query.whereById "@id"}"""
/// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op =
sprintf
"UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s"
tableName (Query.whereByField fieldName op "@field")
/// Parameter handling helpers
[<AutoOpen>]
module Parameters =
/// Create an ID parameter (name "@id", key will be treated as a string)
[<CompiledName "Id">]
let idParam (key: 'TKey) =
SqliteParameter("@id", string key)
/// Create a parameter with a JSON value
[<CompiledName "Json">]
let jsonParam name (it: 'TJson) =
SqliteParameter(name, Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field")
[<CompiledName "Field">]
let fieldParam (value: obj) =
SqliteParameter("@field", value)
/// An empty parameter sequence
[<CompiledName "None">]
let noParams =
Seq.empty<SqliteParameter>
/// Helper functions for handling results
[<AutoOpen>]
module Results =
/// Create a domain item from a document, specifying the field in which the document is found
[<CompiledName "FromDocument">]
let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc =
Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field)))
/// Create a domain item from a document
[<CompiledName "FromData">]
let fromData<'TDoc> rdr =
fromDocument<'TDoc> "data" rdr
/// Create a list of items for the results of the given command, using the specified mapping function
[<CompiledName "FSharpToCustomList">]
let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask {
use! rdr = cmd.ExecuteReaderAsync()
let mutable it = Seq.empty<'TDoc>
while! rdr.ReadAsync() do
it <- Seq.append it (Seq.singleton (mapFunc rdr))
return List.ofSeq it
}
/// Extract a count from the first column
[<CompiledName "ToCount">]
let toCount (row: SqliteDataReader) =
row.GetInt64 0
/// Extract a true/false value from a count in the first column
[<CompiledName "ToExists">]
let toExists row =
toCount(row) > 0L
[<AutoOpen>]
module internal Helpers =
/// Execute a non-query command
let internal write (cmd: SqliteCommand) = backgroundTask {
let! _ = cmd.ExecuteNonQueryAsync()
()
}
/// Versions of queries that accept a SqliteConnection as the last parameter
module WithConn =
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'TDoc)
(conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
toCustomList<'TDoc> cmd mapFunc
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn) = backgroundTask {
let! results = list<'TDoc> query parameters mapFunc.Invoke conn
return ResizeArray<'TDoc> results
}
/// Execute a query that returns one or no results (returns None if not found)
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) conn = backgroundTask {
let! results = list query parameters mapFunc conn
return FSharp.Collections.List.tryHead results
}
/// Execute a query that returns one or no results (returns null if not found)
let Single<'TDoc when 'TDoc: null>(
query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn
) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke conn
return Option.toObj result
}
/// Execute a query that does not return a value
[<CompiledName "NonQuery">]
let nonQuery query (parameters: SqliteParameter seq) (conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
write cmd
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T : struct> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'T)
(conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
use! rdr = cmd.ExecuteReaderAsync()
let! isFound = rdr.ReadAsync()
return if isFound then mapFunc rdr else Unchecked.defaultof<'T>
}
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>, conn) =
scalar<'T> query parameters mapFunc.Invoke conn
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name conn = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn
}
/// Create an index on a document table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) conn =
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")
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn
/// Commands to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName conn =
Custom.scalar (Query.Count.all tableName) [] toCount conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) conn =
Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] toCount conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) conn =
Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] toExists conn
/// Commands to retrieve documents
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName conn =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn
/// Retrieve all documents in the given table
let All<'TDoc>(tableName, conn) =
Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn)
/// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn =
Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) =
Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on a JSON field
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName fieldName op (value: obj) conn =
Custom.list<'TDoc> (Query.Find.byField tableName fieldName op) [ fieldParam value ] fromData<'TDoc> conn
/// Retrieve documents via a comparison on a JSON field
let ByField<'TDoc>(tableName, fieldName, op, value: obj, conn) =
Custom.List<'TDoc>(Query.Find.byField tableName fieldName op, [ fieldParam value ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName fieldName op (value: obj) conn =
Custom.single
$"{Query.Find.byField tableName fieldName op} LIMIT 1" [ fieldParam value ] fromData<'TDoc> conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj, conn) =
Custom.Single(
$"{Query.Find.byField tableName fieldName op} LIMIT 1", [ fieldParam value ], fromData<'TDoc>, conn)
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) conn =
Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn =
byId tableName (idFunc document) document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) =
byFunc tableName idFunc.Invoke document conn
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
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 fieldName op (value: obj) (patch: 'TPatch) (conn: SqliteConnection) =
Custom.nonQuery
(Query.Patch.byField tableName fieldName op) [ fieldParam value; jsonParam "@data" patch ] conn
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) conn =
Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] conn
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn ()
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that returns one or no results (returns None if not found)
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// Execute a query that returns one or no results (returns null if not found)
let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that does not return a value
[<CompiledName "NonQuery">]
let nonQuery query parameters =
use conn = Configuration.dbConn ()
WithConn.Custom.nonQuery query parameters conn
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: SqliteDataReader -> 'T) =
use conn = Configuration.dbConn ()
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureTable name conn
/// Create an index on a document table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// Document insert/save functions
[<AutoOpen>]
module Document =
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.insert tableName 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) =
use conn = Configuration.dbConn ()
WithConn.save tableName document conn
/// Commands to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName =
use conn = Configuration.dbConn ()
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
use conn = Configuration.dbConn ()
WithConn.Count.byField tableName fieldName op value conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
use conn = Configuration.dbConn ()
WithConn.Exists.byField tableName fieldName op value conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.all<'TDoc> tableName conn
/// Retrieve all documents in the given table
let All<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.All<'TDoc>(tableName, conn)
/// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
use conn = Configuration.dbConn ()
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId) =
use conn = Configuration.dbConn ()
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on a JSON field
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName fieldName op value =
use conn = Configuration.dbConn ()
WithConn.Find.byField<'TDoc> tableName fieldName op value conn
/// Retrieve documents via a comparison on a JSON field
let ByField<'TDoc>(tableName, fieldName, op, value) =
use conn = Configuration.dbConn ()
WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName fieldName op value =
use conn = Configuration.dbConn ()
WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn)
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byId tableName docId document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byFunc tableName idFunc document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.ByFunc(tableName, idFunc, document, conn)
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field in the WHERE clause
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byField tableName fieldName op value patch conn
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName fieldName op (value: obj) =
use conn = Configuration.dbConn ()
WithConn.Delete.byField tableName fieldName op value conn

101
src/Sqlite/README.md Normal file
View File

@ -0,0 +1,101 @@
# BitBadger.Documents.Sqlite
This package provides a lightweight document library backed by [SQLite](https://www.sqlite.org). It also provides streamlined functions for traditional ADO.NET functionality where relational data is required. Both C# and F# have first-class implementations.
## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Address documents via ID or via comparison on any field
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions
- Use building blocks for more complex queries
## Getting Started
Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library:
```csharp
// C#
using BitBadger.Documents.Sqlite;
//...
Sqlite.Configuration.UseConnectionString("connection-string");
// A new, open connection to the database can be obtained via
// Sqlite.Configuration.DbConn()
```
```fsharp
// F#
open BitBadger.Documents.Sqlite
// ...
Configuration.useConnectionString "connection-string"
// A new, open connection to the database can be obtained via
// Configuration.dbConn ()
```
By default, the library uses a `System.Text.Json`-based serializer configured to use the `FSharp.SystemTextJson` converter. To provide a different serializer (different options, more converters, etc.), construct it to implement `IDocumentSerializer` and provide it via `Configuration.useSerializer`. If custom serialization makes the serialized Id field not be `Id`, that will also need to be configured.
## Using
Retrieve all customers:
```csharp
// C#; parameter is table name
// Find.All type signature is Func<string, Task<List<TDoc>>>
var customers = await Find.All("customer");
```
```fsharp
// F#
// Find.all type signature is string -> Task<'TDoc list>
let! customers = Find.all<Customer> "customer"
```
Select a customer by ID:
```csharp
// C#; parameters are table name and ID
// Find.ById type signature is Func<string, TKey, Task<TDoc?>>
var customer = await Find.ById<string, Customer>("customer", "123");
```
```fsharp
// F#
// Find.byId type signature is string -> 'TKey -> Task<'TDoc option>
let! customer = Find.byId<string, Customer> "customer" "123"
```
_(keys are treated as strings in the database)_
Count customers in Atlanta:
```csharp
// C#; parameters are table name, field, operator, and value
// Count.ByField type signature is Func<string, string, Op, object, Task<long>>
var customerCount = await Count.ByField("customer", "City", Op.EQ, "Atlanta");
```
```fsharp
// F#
// Count.byField type signature is string -> string -> Op -> obj -> Task<int64>
let! customerCount = Count.byField "customer" "City" EQ "Atlanta"
```
Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp
// C#; parameters are same as above, except return is void
// Delete.ByField type signature is Func<string, string, Op, object, Task>
await Delete.ByField("customer", "City", Op.EQ, "Chicago");
```
```fsharp
// F#
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit>
do! Delete.byField "customer" "City" EQ "Chicago"
```
## More Information
The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library.

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
<ProjectReference Include="..\Postgres\BitBadger.Documents.Postgres.fsproj" />
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" />
<PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,238 @@
using Expecto.CSharp;
using Expecto;
namespace BitBadger.Documents.Tests.CSharp;
using static Runner;
/// <summary>
/// A test serializer that returns known values
/// </summary>
internal class TestSerializer : IDocumentSerializer
{
public string Serialize<T>(T it) => "{\"Overridden\":true}";
public T Deserialize<T>(string it) => default!;
}
/// <summary>
/// C# Tests for common functionality in <tt>BitBadger.Documents</tt>
/// </summary>
public static class CommonCSharpTests
{
/// <summary>
/// Unit tests
/// </summary>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit", new[]
{
TestSequenced(
TestList("Configuration", new[]
{
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");
}
})
})),
TestList("Op", new[]
{
TestCase("EQ succeeds", () =>
{
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("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("Query", new[]
{
TestCase("SelectFromTable succeeds", () =>
{
Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table",
"SELECT statement not correct");
}),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct");
}),
TestList("WhereByField", new[]
{
TestCase("succeeds when a logical operator is passed", () =>
{
Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test",
"WHERE clause not correct");
}),
TestCase("succeeds when an existence operator is passed", () =>
{
Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL",
"WHERE clause 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(
Query.Definition.EnsureIndexOn("test.table", "gibberish",
new[] { "taco", "guac DESC", "salsa ASC" }),
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
+ "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)",
"CREATE INDEX for multiple field statement incorrect");
})
}),
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("Update succeeds", () =>
{
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id",
"UPDATE full statement not correct");
}),
TestList("Count", new[]
{
TestCase("All succeeds", () =>
{
Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ),
"SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field",
"JSON field text comparison count query not correct");
})
}),
TestList("Exists", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Query.Exists.ById("tbl"),
"SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it",
"ID existence query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT),
"SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it",
"JSON field text comparison exists query not correct");
})
}),
TestList("Find", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id",
"SELECT by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE),
"SELECT data FROM tbl WHERE data ->> 'Golf' >= @field",
"SELECT by JSON comparison query not correct");
})
}),
TestList("Delete", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id",
"DELETE by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX),
"DELETE FROM tbl WHERE data ->> 'gone' IS NULL",
"DELETE by JSON comparison query not correct");
})
})
})
});
}

View File

@ -0,0 +1,807 @@
using Expecto.CSharp;
using Expecto;
using BitBadger.Documents.Postgres;
using Npgsql;
namespace BitBadger.Documents.Tests.CSharp;
using static CommonExtensionsAndTypesForNpgsqlFSharp;
using static Runner;
/// <summary>
/// C# tests for the extensions on the <tt>NpgsqlConnection</tt> type
/// </summary>
public class PostgresCSharpExtensionTests
{
private static Task LoadDocs() => PostgresCSharpTests.LoadDocs();
/// <summary>
/// Create a connection to the throwaway database
/// </summary>
/// <param name="db">The throwaway database for which a connection should be made</param>
/// <returns>An open connection to the throwaway database</returns>
private static NpgsqlConnection MkConn(ThrowawayPostgresDb db)
{
var conn = new NpgsqlConnection(db.ConnectionString);
conn.Open();
return conn;
}
/// <summary>
/// Integration tests for the SQLite extension methods
/// </summary>
[Tests]
public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[]
{
TestList("CustomList", new[]
{
TestCase("succeeds when data is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.CustomList(Query.SelectFromTable(PostgresDb.TableName), Parameters.None,
Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}),
TestCase("succeeds when data is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var docs = await conn.CustomList(
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) },
Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("CustomSingle", new[]
{
TestCase("succeeds when a row is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when a row is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("CustomNonQuery", new[]
{
TestCase("succeeds when operating on data", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None);
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 0, "There should be no documents remaining in the table");
}),
TestCase("succeeds when no data matches where clause", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) });
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should be 5 documents remaining in the table");
})
}),
TestCase("Scalar succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value"));
Expect.equal(nbr, 5, "The query should have returned the number 5");
}),
TestCase("EnsureTable succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
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 alsoExists = await keyExists();
Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already");
await conn.EnsureTable("ensured");
exists = await tableExists();
alsoExists = await keyExists();
Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist");
}),
TestCase("EnsureDocumentIndex succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
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();
Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured");
await conn.EnsureDocumentIndex("ensured", DocumentIndex.Optimized);
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestCase("EnsureFieldIndex succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
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();
Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured");
await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestList("Insert", new[]
{
TestCase("succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await conn.Insert(PostgresDb.TableName,
new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } });
var after = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("fails for duplicate key", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" });
try
{
await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" });
Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert");
}
catch (Exception)
{
// This is what should have happened
}
})
}),
TestList("save", new[]
{
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await conn.Save(PostgresDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var after = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await conn.Insert(PostgresDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var before = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "test");
Expect.isNotNull(before, "There should have been a document returned");
Expect.equal(before.Id, "test", "The document is not correct");
await conn.Save(PostgresDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "c", Bar = "d" } });
var after = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "test");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct");
})
}),
TestCase("CountAll succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var theCount = await conn.CountAll(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents");
}),
TestCase("CountByField succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var theCount = await conn.CountByField(PostgresDb.TableName, "Value", Op.EQ, "purple");
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
TestCase("CountByContains succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var theCount = await conn.CountByContains(PostgresDb.TableName, new { Value = "purple" });
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
TestCase("CountByJsonPath succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents");
}),
TestList("ExistsById", new[]
{
TestCase("succeeds when a document exists", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsById(PostgresDb.TableName, "three");
Expect.isTrue(exists, "There should have been an existing document");
}),
TestCase("succeeds when a document does not exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ExistsByField", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByField(PostgresDb.TableName, "Sub", Op.EX, "");
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when documents do not exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByField(PostgresDb.TableName, "NumValue", Op.EQ, "six");
Expect.isFalse(exists, "There should not have been existing documents");
})
}),
TestList("ExistsByContains", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByContains(PostgresDb.TableName, new { NumValue = 10 });
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("ExistsByJsonPath", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")");
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("FindAll", new[]
{
TestCase("succeeds when there is data", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "one" });
await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "three" });
await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "five" });
var results = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.equal(results.Count, 3, "There should have been 3 documents returned");
}),
TestCase("succeeds when there is no data", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var results = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("FindById", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "two");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The 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.FindById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FindByField", new[]
{
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.FindByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "another");
Expect.equal(docs.Count, 1, "There should have been one document returned");
}),
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.FindByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "mauve");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindByContains", new[]
{
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.FindByContains<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } });
Expect.equal(docs.Count, 2, "There should have been two documents returned");
}),
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.FindByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindByJsonPath", new[]
{
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.FindByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 15)");
Expect.equal(docs.Count, 3, "There should have been 3 documents returned");
}),
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.FindByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindFirstByField", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "another");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "purple");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "five", "four" }, 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.FindFirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "absent");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FindFirstByContains", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "another" });
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByContains<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } });
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, 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.FindFirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" });
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FindFirstByJsonPath", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName,
"$.Value ? (@ == \"FIRST!\")");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName,
"$.Sub.Foo ? (@ == \"green\")");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, 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.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("UpdateById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.UpdateById(PostgresDb.TableName, "one",
new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } });
var after = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct (ID)");
Expect.equal(after.Value, "", "The updated document is not correct (Value)");
Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)");
Expect.isNotNull(after.Sub, "The updated document should have had a sub-document");
Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)");
Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.UpdateById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("UpdateByFunc", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
var after = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct (ID)");
Expect.equal(after.Value, "le un", "The updated document is not correct (Value)");
Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)");
Expect.isNull(after.Sub, "The updated document should not have had a sub-document");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
TestList("PatchById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.PatchById(PostgresDb.TableName, "one", new { NumValue = 44 });
var after = await conn.FindById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.NumValue, 44, "The updated document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("PatchByField", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.PatchByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 });
var after = await conn.CountByField(PostgresDb.TableName, "NumValue", Op.EQ, "77");
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" });
})
}),
TestList("PatchByContains", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.PatchByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 });
var after = await conn.CountByContains(PostgresDb.TableName, new { NumValue = 77 });
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
})
}),
TestList("PatchByJsonPath", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 });
var after = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)");
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
var before = await conn.CountAll(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
})
}),
TestList("DeleteById", new[]
{
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteById(PostgresDb.TableName, "four");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 4, "There should have been 4 documents remaining");
}),
TestCase("succeeds when a document is not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteById(PostgresDb.TableName, "thirty");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByField", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByField(PostgresDb.TableName, "Value", Op.NE, "purple");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 2, "There should have been 2 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByField(PostgresDb.TableName, "Value", Op.EQ, "crimson");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByContains", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByContains(PostgresDb.TableName, new { Value = "purple" });
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByContains(PostgresDb.TableName, new { Value = "crimson" });
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByJsonPath", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await using var conn = MkConn(db);
await LoadDocs();
await conn.DeleteByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)");
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
});
}

View File

@ -0,0 +1,970 @@
using Expecto.CSharp;
using Expecto;
using BitBadger.Documents.Postgres;
using ThrowawayDb.Postgres;
namespace BitBadger.Documents.Tests.CSharp;
using static CommonExtensionsAndTypesForNpgsqlFSharp;
using static Runner;
/// <summary>
/// C# tests for the PostgreSQL implementation of <tt>BitBadger.Documents</tt>
/// </summary>
public class PostgresCSharpTests
{
/// <summary>
/// Tests which do not hit the database
/// </summary>
private static readonly Test Unit = TestList("Unit", new[]
{
TestList("Parameters", new[]
{
TestCase("Id succeeds", () =>
{
var it = Parameters.Id(88);
Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly");
Expect.equal(it.Item2, Sql.@string("88"), "ID parameter value incorrect");
}),
TestCase("Json succeeds", () =>
{
var it = Parameters.Json("@test", new { Something = "good" });
Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly");
Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect");
}),
TestCase("Field succeeds", () =>
{
var it = Parameters.Field(242);
Expect.equal(it.Item1, "@field", "Field parameter not constructed correctly");
Expect.isTrue(it.Item2.IsParameter, "Field parameter value incorrect");
}),
TestCase("None succeeds", () =>
{
Expect.isEmpty(Parameters.None, "The no-params sequence should be empty");
})
}),
TestList("Query", new[]
{
TestList("Definition", new[]
{
TestCase("EnsureTable succeeds", () =>
{
Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName),
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)",
"CREATE TABLE statement not constructed correctly");
}),
TestCase("EnsureDocumentIndex succeeds for full index", () =>
{
Expect.equal(Postgres.Query.Definition.EnsureDocumentIndex("schema.tbl", DocumentIndex.Full),
"CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)",
"CREATE INDEX statement not constructed correctly");
}),
TestCase("EnsureDocumentIndex succeeds for JSONB Path Ops index", () =>
{
Expect.equal(
Postgres.Query.Definition.EnsureDocumentIndex(PostgresDb.TableName, DocumentIndex.Optimized),
string.Format(
"CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)",
PostgresDb.TableName),
"CREATE INDEX statement not constructed correctly");
})
}),
TestCase("WhereDataContains succeeds", () =>
{
Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test",
"WHERE clause not correct");
}),
TestCase("WhereJsonPathMatches succeeds", () =>
{
Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath",
"WHERE clause not correct");
}),
TestList("Count", new[]
{
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName),
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria",
"JSON containment count query not correct");
}),
TestCase("ByJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.Count.ByJsonPath(PostgresDb.TableName),
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"JSON Path match count query not correct");
})
}),
TestList("Exists", new[]
{
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName),
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it",
"JSON containment exists query not correct");
}),
TestCase("byJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ByJsonPath(PostgresDb.TableName),
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it",
"JSON Path match existence query not correct");
})
}),
TestList("Find", new[]
{
TestCase("byContains succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName),
$"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria",
"SELECT by JSON containment query not correct");
}),
TestCase("byJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ByJsonPath(PostgresDb.TableName),
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"SELECT by JSON Path match query not correct");
})
}),
TestList("Patch", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id",
"UPDATE partial by ID statement not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, "Snail", Op.LT),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field",
"UPDATE partial by ID statement not correct");
}),
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.Patch.ByContains(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria",
"UPDATE partial by JSON containment statement not correct");
}),
TestCase("ByJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.Patch.ByJsonPath(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath",
"UPDATE partial by JSON Path statement not correct");
})
}),
TestList("Delete", new[]
{
TestCase("byContains succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName),
$"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria",
"DELETE by JSON containment query not correct");
}),
TestCase("byJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ByJsonPath(PostgresDb.TableName),
$"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
"DELETE by JSON Path match query not correct");
})
})
})
});
private static readonly List<JsonDocument> TestDocuments = new()
{
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
};
/// <summary>
/// Add the test documents to the database
/// </summary>
internal static async Task LoadDocs()
{
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
/// <summary>
/// Integration tests for the PostgreSQL library
/// </summary>
private static readonly Test Integration = TestList("Integration", new[]
{
TestList("Configuration", new[]
{
TestCase("UseDataSource disposes existing source", () =>
{
using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value);
var source = PostgresDb.MkDataSource(db1.ConnectionString);
Postgres.Configuration.UseDataSource(source);
using var db2 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value);
Postgres.Configuration.UseDataSource(PostgresDb.MkDataSource(db2.ConnectionString));
try
{
_ = source.OpenConnection();
Expect.isTrue(false, "Data source should have been disposed");
}
catch (Exception)
{
// This is what should have happened
}
}),
TestCase("DataSource returns configured data source", () =>
{
using var db = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value);
var source = PostgresDb.MkDataSource(db.ConnectionString);
Postgres.Configuration.UseDataSource(source);
Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()),
"Data source should have been the same");
})
}),
TestList("Custom", new[]
{
TestList("List", new[]
{
TestCase("succeeds when data is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Custom.List(Query.SelectFromTable(PostgresDb.TableName), Parameters.None,
Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}),
TestCase("succeeds when data is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Custom.List(
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) },
Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("Single", new[]
{
TestCase("succeeds when a row is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when a row is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("NonQuery", new[]
{
TestCase("succeeds when operating on data", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None);
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 0, "There should be no documents remaining in the table");
}),
TestCase("succeeds when no data matches where clause", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath",
new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) });
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should be 5 documents remaining in the table");
})
}),
TestCase("Scalar succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value"));
Expect.equal(nbr, 5, "The query should have returned the number 5");
})
}),
TestList("Definition", new[]
{
TestCase("EnsureTable succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var tableExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None,
Results.ToExists);
var keyExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None,
Results.ToExists);
var exists = await tableExists();
var alsoExists = await keyExists();
Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already");
await Definition.EnsureTable("ensured");
exists = await tableExists();
alsoExists = await keyExists();
Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist");
}),
TestCase("EnsureDocumentIndex succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized);
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestCase("EnsureFieldIndex succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var indexExists = () => Custom.Scalar(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None,
Results.ToExists);
var exists = await indexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
})
}),
TestList("Document", new[]
{
TestList("Insert", new[]
{
TestCase("succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await Document.Insert(PostgresDb.TableName,
new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } });
var after = await Count.All(PostgresDb.TableName);
Expect.equal(after, 1, "There should have been one document inserted");
}),
TestCase("fails for duplicate key", async () =>
{
await using var db = PostgresDb.BuildDb();
await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" });
try
{
await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" });
Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert");
}
catch (Exception)
{
// This is what should have happened
}
})
}),
TestList("Save", new[]
{
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should be no documents in the table");
await Document.Save(PostgresDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var after = await Count.All(PostgresDb.TableName);
Expect.equal(after, 1, "There should have been one document inserted");
}),
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await Document.Insert(PostgresDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var before = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "test");
Expect.isNotNull(before, "There should have been a document returned");
Expect.equal(before.Id, "test", "The document is not correct");
before.Sub = new() { Foo = "c", Bar = "d" };
await Document.Save(PostgresDb.TableName, before);
var after = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "test");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "test", "The document is not correct");
Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct");
})
})
}),
TestList("Count", new[]
{
TestCase("All succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.All(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents");
}),
TestCase("ByField succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(PostgresDb.TableName, "Value", Op.EQ, "purple");
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
TestCase("ByContains succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByContains(PostgresDb.TableName, new { Value = "purple" });
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
TestCase("ByJsonPath succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents");
})
}),
TestList("Exists", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document exists", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ById(PostgresDb.TableName, "three");
Expect.isTrue(exists, "There should have been an existing document");
}),
TestCase("succeeds when a document does not exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByField(PostgresDb.TableName, "Sub", Op.NEX, "");
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when documents do not exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByField(PostgresDb.TableName, "NumValue", Op.EQ, "six");
Expect.isFalse(exists, "There should not have been existing documents");
})
}),
TestList("ByContains", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByContains(PostgresDb.TableName, new { NumValue = 10 });
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("ByJsonPath", new[] {
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")");
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents");
})
})
}),
TestList("Find", new[]
{
TestList("All", new[]
{
TestCase("succeeds when there is data", async () =>
{
await using var db = PostgresDb.BuildDb();
await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "one", Bar = "two" });
await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "three", Bar = "four" });
await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "five", Bar = "six" });
var results = await Find.All<SubDocument>(PostgresDb.TableName);
Expect.equal(results.Count, 3, "There should have been 3 documents returned");
}),
TestCase("succeeds when there is no data", async () =>
{
await using var db = PostgresDb.BuildDb();
var results = await Find.All<SubDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("ById", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "two");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "three hundred eighty-seven");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "another");
Expect.equal(docs.Count, 1, "There should have been one document returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "mauve");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("ByContains", new[]
{
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByContains<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } });
Expect.equal(docs.Count, 2, "There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("ByJsonPath", new[]
{
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 15)");
Expect.equal(docs.Count, 3, "There should have been 3 documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var docs = await Find.ByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FirstByField", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "another");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "purple");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(PostgresDb.TableName, "Value", Op.EQ, "absent");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FirstByContains", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "another" });
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName,
new { Sub = new { Foo = "green" } });
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByContains<JsonDocument>(PostgresDb.TableName, new { Value = "absent" });
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FirstByJsonPath", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName,
"$.Value ? (@ == \"FIRST!\")");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName,
"$.Sub.Foo ? (@ == \"green\")");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned");
})
})
}),
TestList("Update", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Update.ById(PostgresDb.TableName, "one",
new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } });
var after = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct (ID)");
Expect.equal(after.Value, "", "The updated document is not correct (Value)");
Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)");
Expect.isNotNull(after.Sub, "The updated document should have had a sub-document");
Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)");
Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Update.ById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("ByFunc", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Update.ByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
var after = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct (ID)");
Expect.equal(after.Value, "le un", "The updated document is not correct (Value)");
Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)");
Expect.isNull(after.Sub, "The updated document should not have had a sub-document");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Update.ByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
})
}),
TestList("Patch", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Patch.ById(PostgresDb.TableName, "one", new { NumValue = 44 });
var after = await Find.ById<string, JsonDocument>(PostgresDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.NumValue, 44, "The updated document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Patch.ByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 });
var after = await Count.ByField(PostgresDb.TableName, "NumValue", Op.EQ, "77");
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" });
})
}),
TestList("ByContains", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Patch.ByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 });
var after = await Count.ByContains(PostgresDb.TableName, new { NumValue = 77 });
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
})
}),
TestList("ByJsonPath", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 });
var after = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)");
Expect.equal(after, 2, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
var before = await Count.All(PostgresDb.TableName);
Expect.equal(before, 0, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
})
})
}),
TestList("Delete", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ById(PostgresDb.TableName, "four");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 4, "There should have been 4 documents remaining");
}),
TestCase("succeeds when a document is not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ById(PostgresDb.TableName, "thirty");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByField(PostgresDb.TableName, "Value", Op.EQ, "purple");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByField(PostgresDb.TableName, "Value", Op.EQ, "crimson");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByContains", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByContains(PostgresDb.TableName, new { Value = "purple" });
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByContains(PostgresDb.TableName, new { Value = "crimson" });
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("ByJsonPath", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 3, "There should have been 3 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
await Delete.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)");
var remaining = await Count.All(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
})
})
});
/// <summary>
/// All Postgres C# tests
/// </summary>
[Tests]
public static readonly Test All = TestList("Postgres.C#", new[] { Unit, TestSequenced(Integration) });
}

View File

@ -0,0 +1,150 @@
using Npgsql;
using Npgsql.FSharp;
using ThrowawayDb.Postgres;
namespace BitBadger.Documents.Tests;
/// <summary>
/// A throwaway SQLite database file, which will be deleted when it goes out of scope
/// </summary>
public class ThrowawayPostgresDb : IDisposable, IAsyncDisposable
{
private readonly ThrowawayDatabase _db;
/// <summary>
/// The connection string for the throwaway database
/// </summary>
public string ConnectionString => _db.ConnectionString;
/// <summary>
/// Constructor
/// </summary>
/// <param name="db">The throwaway database which this instance will wrap</param>
public ThrowawayPostgresDb(ThrowawayDatabase db)
{
_db = db;
}
public void Dispose()
{
_db.Dispose();
GC.SuppressFinalize(this);
}
public ValueTask DisposeAsync()
{
_db.Dispose();
GC.SuppressFinalize(this);
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Database helpers for PostgreSQL integration tests
/// </summary>
public static class PostgresDb
{
/// <summary>
/// The name of the table used for testing
/// </summary>
public const string TableName = "test_table";
/// <summary>
/// The host for the database
/// </summary>
private static readonly Lazy<string> DbHost = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbHost") switch
{
null => "localhost",
var host when host.Trim() == "" => "localhost",
var host => host
};
});
/// <summary>
/// The port for the database
/// </summary>
private static readonly Lazy<int> DbPort = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbPort") switch
{
null => 5432,
var port when port.Trim() == "" => 5432,
var port => int.Parse(port)
};
});
/// <summary>
/// The database itself
/// </summary>
private static readonly Lazy<string> DbDatabase = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postres.DbDatabase") switch
{
null => "postgres",
var db when db.Trim() == "" => "postgres",
var db => db
};
});
/// <summary>
/// The user to use in connecting to the database
/// </summary>
private static readonly Lazy<string> DbUser = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbUser") switch
{
null => "postgres",
var user when user.Trim() == "" => "postgres",
var user => user
};
});
/// <summary>
/// The password to use for the database
/// </summary>
private static readonly Lazy<string> DbPassword = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postrgres.DbPwd") switch
{
null => "postgres",
var pwd when pwd.Trim() == "" => "postgres",
var pwd => pwd
};
});
/// <summary>
/// The overall connection string
/// </summary>
public static readonly Lazy<string> ConnStr = new(() =>
Sql.formatConnectionString(
Sql.password(DbPassword.Value,
Sql.username(DbUser.Value,
Sql.database(DbDatabase.Value,
Sql.port(DbPort.Value,
Sql.host(DbHost.Value)))))));
/// <summary>
/// Create a data source using the derived connection string
/// </summary>
public static NpgsqlDataSource MkDataSource(string cStr) =>
new NpgsqlDataSourceBuilder(cStr).Build();
/// <summary>
/// Build the throwaway database
/// </summary>
public static ThrowawayPostgresDb BuildDb()
{
var database = ThrowawayDatabase.Create(ConnStr.Value);
var sqlProps = Sql.connect(database.ConnectionString);
Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps));
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps));
Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString));
return new ThrowawayPostgresDb(database);
}
}

View File

@ -0,0 +1,518 @@
using Expecto.CSharp;
using Expecto;
using Microsoft.Data.Sqlite;
using BitBadger.Documents.Sqlite;
namespace BitBadger.Documents.Tests.CSharp;
using static Runner;
/// <summary>
/// C# tests for the extensions on the <tt>SqliteConnection</tt> class
/// </summary>
public static class SqliteCSharpExtensionTests
{
private static Task LoadDocs() => SqliteCSharpTests.LoadDocs();
/// <summary>
/// Integration tests for the SQLite extension methods
/// </summary>
[Tests]
public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[]
{
TestList("CustomSingle", new[]
{
TestCase("succeeds when a row is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("one") }, Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when a row is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("CustomList", new[]
{
TestCase("succeeds when data is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var docs = await conn.CustomList(Query.SelectFromTable(SqliteDb.TableName), Parameters.None,
Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}),
TestCase("succeeds when data is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var docs = await conn.CustomList(
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value",
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("CustomNonQuery", new[]
{
TestCase("succeeds when operating on data", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None);
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 0L, "There should be no documents remaining in the table");
}),
TestCase("succeeds when no data matches where clause", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value",
new[] { new SqliteParameter("@value", 100) });
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table");
})
}),
TestCase("CustomScalar succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0));
Expect.equal(nbr, 5, "The query should have returned the number 5");
}),
TestCase("EnsureTable succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
Func<string, ValueTask<bool>> itExists = async name =>
await conn.CustomScalar(
$"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(alsoExists, "The key index should not exist already");
await conn.EnsureTable("ensured");
exists = await itExists("ensured");
alsoExists = await itExists("idx_ensured_key");
Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist");
}),
TestCase("EnsureFieldIndex succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
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();
Expect.isFalse(exists, "The index should not exist already");
await conn.EnsureTable("ensured");
await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestList("Insert", new[]
{
TestCase("succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should be no documents in the table");
await conn.Insert(SqliteDb.TableName,
new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } });
var after = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("fails for duplicate key", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" });
try
{
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" });
Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert");
}
catch (Exception)
{
// This is what is supposed to happen
}
})
}),
TestList("Save", new[]
{
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should be no documents in the table");
await conn.Save(SqliteDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var after = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await conn.Insert(SqliteDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var before = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "test");
Expect.isNotNull(before, "There should have been a document returned");
Expect.equal(before!.Id, "test", "The document is not correct");
Expect.isNotNull(before.Sub, "There should have been a sub-document");
Expect.equal(before.Sub!.Foo, "a", "The document is not correct");
Expect.equal(before.Sub.Bar, "b", "The document is not correct");
await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" });
var after = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "test");
Expect.isNotNull(after, "There should have been a document returned post-update");
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");
})
}),
TestCase("CountAll succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var theCount = await conn.CountAll(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents");
}),
TestCase("CountByField succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple");
Expect.equal(theCount, 2L, "There should have been 2 matching documents");
}),
TestList("ExistsById", new[]
{
TestCase("succeeds when a document exists", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var exists = await conn.ExistsById(SqliteDb.TableName, "three");
Expect.isTrue(exists, "There should have been an existing document");
}),
TestCase("succeeds when a document does not exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var exists = await conn.ExistsById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ExistsByField", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var exists = await conn.ExistsByField(SqliteDb.TableName, "NumValue", Op.GE, 10);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var exists = await conn.ExistsByField(SqliteDb.TableName, "Nothing", Op.EQ, "none");
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("FindAll", new[]
{
TestCase("succeeds when there is data", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" });
await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" });
await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" });
var results = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.equal(results.Count, 3, "There should have been 3 documents returned");
}),
TestCase("succeeds when there is no data", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var results = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("FindById", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "two");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "eighty-seven");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FindByField", new[]
{
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.FindByField<JsonDocument>(SqliteDb.TableName, "NumValue", Op.GT, 15);
Expect.equal(docs.Count, 2, "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.FindByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "mauve");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindFirstByField", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "another");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, "Sub.Foo", Op.EQ, "green");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "absent");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("UpdateById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } };
await conn.UpdateById(SqliteDb.TableName, "one", testDoc);
var after = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct");
Expect.isNotNull(after.Sub, "The updated document should have had a sub-document");
Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct");
Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await conn.UpdateById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("UpdateByFunc", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
var after = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is incorrect");
Expect.equal(after.Value, "le un", "The updated document is incorrect");
Expect.equal(after.NumValue, 1, "The updated document is incorrect");
Expect.isNull(after.Sub, "The updated document should not have a sub-document");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
TestList("PatchById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.PatchById(SqliteDb.TableName, "one", new { NumValue = 44 });
var after = await conn.FindById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after.Id, "one", "The updated document is not correct");
Expect.equal(after.NumValue, 44, "The updated document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("PatchByField", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.PatchByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 });
var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 77);
Expect.equal(after, 2L, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
var before = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await conn.PatchByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" });
})
}),
TestList("DeleteById", new[]
{
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteById(SqliteDb.TableName, "four");
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 4L, "There should have been 4 documents remaining");
}),
TestCase("succeeds when a document is not deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteById(SqliteDb.TableName, "thirty");
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByField", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteByField(SqliteDb.TableName, "Value", Op.NE, "purple");
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 2L, "There should have been 2 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await using var conn = Sqlite.Configuration.DbConn();
await LoadDocs();
await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "crimson");
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
});
}

View File

@ -0,0 +1,596 @@
using Expecto.CSharp;
using Expecto;
using Microsoft.Data.Sqlite;
using Microsoft.FSharp.Core;
using BitBadger.Documents.Sqlite;
namespace BitBadger.Documents.Tests.CSharp;
using static Runner;
/// <summary>
/// C# tests for the SQLite implementation of <tt>BitBadger.Documents</tt>
/// </summary>
public static class SqliteCSharpTests
{
/// <summary>
/// Unit tests for the SQLite library
/// </summary>
private static readonly Test Unit = TestList("Unit", new[]
{
TestList("Query", new[]
{
TestCase("Definition.EnsureTable succeeds", () =>
{
Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"),
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct");
}),
TestList("Patch", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Patch.ById("tbl"),
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id",
"UPDATE partial by ID statement not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Sqlite.Query.Patch.ByField("tbl", "Part", Op.NE),
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field",
"UPDATE partial by JSON comparison query not correct");
})
}),
}),
TestList("Parameters", new[]
{
TestCase("Id succeeds", () =>
{
var theParam = Parameters.Id(7);
Expect.equal(theParam.ParameterName, "@id", "The parameter name is incorrect");
Expect.equal(theParam.Value, "7", "The parameter value is incorrect");
}),
TestCase("Json succeeds", () =>
{
var theParam = Parameters.Json("@test", new { Nice = "job" });
Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect");
Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect");
}),
TestCase("Field succeeds", () =>
{
var theParam = Parameters.Field(99);
Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect");
Expect.equal(theParam.Value, 99, "The parameter value is incorrect");
}),
TestCase("None succeeds", () =>
{
Expect.isEmpty(Parameters.None, "The parameter list should have been empty");
})
})
// Results are exhaustively executed in the context of other tests
});
private static readonly List<JsonDocument> TestDocuments = new()
{
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
};
/// <summary>
/// Add the test documents to the database
/// </summary>
internal static async Task LoadDocs()
{
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
private static readonly Test Integration = TestList("Integration", new[]
{
TestCase("Configuration.UseConnectionString succeeds", () =>
{
try
{
Sqlite.Configuration.UseConnectionString("Data Source=test.db");
Expect.equal(Sqlite.Configuration.connectionString,
new FSharpOption<string>("Data Source=test.db;Foreign Keys=True"), "Connection string incorrect");
}
finally
{
Sqlite.Configuration.UseConnectionString("Data Source=:memory:");
}
}),
TestList("Custom", new[]
{
TestList("Single", new[]
{
TestCase("succeeds when a row is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("one") }, Results.FromData<JsonDocument>);
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "one", "The incorrect document was returned");
}),
TestCase("succeeds when a row is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id",
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("List", new[]
{
TestCase("succeeds when data is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Custom.List(Query.SelectFromTable(SqliteDb.TableName), Parameters.None,
Results.FromData<JsonDocument>);
Expect.equal(docs.Count, 5, "There should have been 5 documents returned");
}),
TestCase("succeeds when data is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Custom.List(
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value",
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("NonQuery", new[]
{
TestCase("succeeds when operating on data", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None);
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 0L, "There should be no documents remaining in the table");
}),
TestCase("succeeds when no data matches where clause", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value",
new[] { new SqliteParameter("@value", 100) });
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table");
})
}),
TestCase("Scalar succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0));
Expect.equal(nbr, 5, "The query should have returned the number 5");
})
}),
TestList("Definition", new[]
{
TestCase("EnsureTable succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
var exists = await ItExists("ensured");
var alsoExists = await ItExists("idx_ensured_key");
Expect.isFalse(exists, "The table should not exist already");
Expect.isFalse(alsoExists, "The key index should not exist already");
await Definition.EnsureTable("ensured");
exists = await ItExists("ensured");
alsoExists = await ItExists("idx_ensured_key");
Expect.isTrue(exists, "The table should now exist");
Expect.isTrue(alsoExists, "The key index should now exist");
return;
async ValueTask<bool> ItExists(string name)
{
return await Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it",
new SqliteParameter[] { new("@name", name) }, Results.ToExists);
}
}),
TestCase("EnsureFieldIndex succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
var indexExists = () => Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
var exists = await indexExists();
Expect.isFalse(exists, "The index should not exist already");
await Definition.EnsureTable("ensured");
await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" });
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
})
}),
TestList("Document.Insert", new[]
{
TestCase("succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should be no documents in the table");
await Document.Insert(SqliteDb.TableName,
new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } });
var after = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("fails for duplicate key", async () =>
{
await using var db = await SqliteDb.BuildDb();
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" });
try
{
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" });
Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert");
}
catch (Exception)
{
// This is what is supposed to happen
}
})
}),
TestList("Document.Save", new[]
{
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should be no documents in the table");
await Document.Save(SqliteDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var after = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.equal(after.Count, 1, "There should have been one document inserted");
}),
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await Document.Insert(SqliteDb.TableName,
new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } });
var before = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "test");
Expect.isNotNull(before, "There should have been a document returned");
Expect.equal(before!.Id, "test", "The document is not correct");
Expect.isNotNull(before.Sub, "There should have been a sub-document");
Expect.equal(before.Sub!.Foo, "a", "The document is not correct");
Expect.equal(before.Sub.Bar, "b", "The document is not correct");
await Document.Save(SqliteDb.TableName, new JsonDocument { Id = "test" });
var after = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "test");
Expect.isNotNull(after, "There should have been a document returned post-update");
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");
})
}),
TestList("Count", new[]
{
TestCase("All succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var theCount = await Count.All(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents");
}),
TestCase("ByField succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple");
Expect.equal(theCount, 2L, "There should have been 2 matching documents");
})
}),
TestList("Exists", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document exists", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ById(SqliteDb.TableName, "three");
Expect.isTrue(exists, "There should have been an existing document");
}),
TestCase("succeeds when a document does not exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByField(SqliteDb.TableName, "NumValue", Op.GE, 10);
Expect.isTrue(exists, "There should have been existing documents");
}),
TestCase("succeeds when no matching documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var exists = await Exists.ByField(SqliteDb.TableName, "Nothing", Op.EQ, "none");
Expect.isFalse(exists, "There should not have been any existing documents");
})
})
}),
TestList("Find", new[]
{
TestList("All", new[]
{
TestCase("succeeds when there is data", async () =>
{
await using var db = await SqliteDb.BuildDb();
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" });
await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" });
var results = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.equal(results.Count, 3, "There should have been 3 documents returned");
}),
TestCase("succeeds when there is no data", async () =>
{
await using var db = await SqliteDb.BuildDb();
var results = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("ById", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "two");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "twenty two");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, "NumValue", Op.GT, 15);
Expect.equal(docs.Count, 2, "There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "mauve");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FirstByField", new[]
{
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "another");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.equal(doc!.Id, "two", "The incorrect document was returned");
}),
TestCase("succeeds when multiple documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, "Sub.Foo", Op.EQ, "green");
Expect.isNotNull(doc, "There should have been a document returned");
Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned");
}),
TestCase("succeeds when a document is not found", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, "Value", Op.EQ, "absent");
Expect.isNull(doc, "There should not have been a document returned");
})
})
}),
TestList("Update", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } };
await Update.ById(SqliteDb.TableName, "one", testDoc);
var after = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after!.Id, "one", "The updated document is not correct");
Expect.isNotNull(after.Sub, "The updated document should have had a sub-document");
Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct");
Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await Update.ById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("ByFunc", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Update.ByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
var after = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after!.Id, "one", "The updated document is incorrect");
Expect.equal(after.Value, "le un", "The updated document is incorrect");
Expect.equal(after.NumValue, 1, "The updated document is incorrect");
Expect.isNull(after.Sub, "The updated document should not have a sub-document");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await Update.ByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
}),
TestList("Patch", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Patch.ById(SqliteDb.TableName, "one", new { NumValue = 44 });
var after = await Find.ById<string, JsonDocument>(SqliteDb.TableName, "one");
Expect.isNotNull(after, "There should have been a document returned post-update");
Expect.equal(after!.Id, "one", "The updated document is not correct");
Expect.equal(after.NumValue, 44, "The updated document is not correct");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Patch.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 });
var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 77);
Expect.equal(after, 2L, "There should have been 2 documents returned");
}),
TestCase("succeeds when no document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
var before = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(before, "There should have been no documents returned");
// This not raising an exception is the test
await Patch.ByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" });
})
})
}),
TestList("Delete", new[]
{
TestList("ById", new[]
{
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ById(SqliteDb.TableName, "four");
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 4L, "There should have been 4 documents remaining");
}),
TestCase("succeeds when a document is not deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ById(SqliteDb.TableName, "thirty");
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestList("ByField", new[]
{
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ByField(SqliteDb.TableName, "Value", Op.NE, "purple");
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 2L, "There should have been 2 documents remaining");
}),
TestCase("succeeds when documents are not deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
await Delete.ByField(SqliteDb.TableName, "Value", Op.EQ, "crimson");
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
})
}),
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
});
/// <summary>
/// All tests for SQLite C# functions and methods
/// </summary>
[Tests]
public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) });
}

View File

@ -0,0 +1,59 @@
namespace BitBadger.Documents.Tests;
using System;
using System.IO;
using System.Threading.Tasks;
using Sqlite;
/// <summary>
/// A throwaway SQLite database file, which will be deleted when it goes out of scope
/// </summary>
public class ThrowawaySqliteDb : IDisposable, IAsyncDisposable
{
private readonly string _dbName;
public ThrowawaySqliteDb(string dbName)
{
_dbName = dbName;
}
public void Dispose()
{
if (File.Exists(_dbName)) File.Delete(_dbName);
GC.SuppressFinalize(this);
}
public ValueTask DisposeAsync()
{
if (File.Exists(_dbName)) File.Delete(_dbName);
GC.SuppressFinalize(this);
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Utility functions for dealing with SQLite databases
/// </summary>
public static class SqliteDb
{
/// <summary>
/// The table name for the catalog metadata
/// </summary>
public const string Catalog = "sqlite_master";
/// <summary>
/// The name of the table used for testing
/// </summary>
public const string TableName = "test_table";
/// <summary>
/// Create a throwaway database file with the test_table defined
/// </summary>
public static async Task<ThrowawaySqliteDb> BuildDb()
{
var dbName = $"test-db-{Guid.NewGuid():n}.db";
Configuration.UseConnectionString($"data source={dbName}");
await Definition.EnsureTable(TableName);
return new ThrowawaySqliteDb(dbName);
}
}

15
src/Tests.CSharp/Types.cs Normal file
View File

@ -0,0 +1,15 @@
namespace BitBadger.Documents.Tests.CSharp;
public class SubDocument
{
public string Foo { get; set; } = "";
public string Bar { get; set; } = "";
}
public class JsonDocument
{
public string Id { get; set; } = "";
public string Value { get; set; } = "";
public int NumValue { get; set; } = 0;
public SubDocument? Sub { get; set; } = null;
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="CommonTests.fs" />
<Compile Include="Types.fs" />
<Compile Include="PostgresTests.fs" />
<Compile Include="PostgresExtensionTests.fs" />
<Compile Include="SqliteTests.fs" />
<Compile Include="SqliteExtensionTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
<ProjectReference Include="..\Postgres\BitBadger.Documents.Postgres.fsproj" />
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
<ProjectReference Include="..\Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj" />
</ItemGroup>
</Project>

159
src/Tests/CommonTests.fs Normal file
View File

@ -0,0 +1,159 @@
module CommonTests
open BitBadger.Documents
open Expecto
/// Test table name
let tbl = "test_table"
/// Tests which do not hit the database
let all =
testList "Common" [
testList "Op" [
test "EQ succeeds" {
Expect.equal (string EQ) "=" "The equals operator was not correct"
}
test "GT succeeds" {
Expect.equal (string GT) ">" "The greater than operator was not correct"
}
test "GE succeeds" {
Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct"
}
test "LT succeeds" {
Expect.equal (string LT) "<" "The less than operator was not correct"
}
test "LE succeeds" {
Expect.equal (string LE) "<=" "The less than or equal to operator was not correct"
}
test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct"
}
test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct"""
}
test "NEX succeeds" {
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct"""
}
]
testList "Query" [
test "selectFromTable succeeds" {
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct"
}
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct"
}
testList "whereByField" [
test "succeeds when a logical operator is passed" {
Expect.equal
(Query.whereByField "theField" GT "@test")
"data ->> 'theField' > @test"
"WHERE clause not correct"
}
test "succeeds when an existence operator is passed" {
Expect.equal
(Query.whereByField "thatField" NEX "")
"data ->> 'thatField' IS NULL"
"WHERE clause not correct"
}
]
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
(Query.Definition.ensureTableFor "my.table" "JSONB")
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
test "ensureIndexOn succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ])
([ "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 "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 "update succeeds" {
Expect.equal
(Query.update tbl)
$"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id"
"UPDATE full statement not correct"
}
testList "Count" [
test "all succeeds" {
Expect.equal (Query.Count.all tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Count.byField tbl "thatField" EQ)
$"SELECT COUNT(*) AS it FROM {tbl} WHERE data ->> 'thatField' = @field"
"JSON field text comparison count query not correct"
}
]
testList "Exists" [
test "byId succeeds" {
Expect.equal
(Query.Exists.byId tbl)
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE data ->> 'Id' = @id) AS it"
"ID existence query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Exists.byField tbl "Test" LT)
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE data ->> 'Test' < @field) AS it"
"JSON field text comparison exists query not correct"
}
]
testList "Find" [
test "byId succeeds" {
Expect.equal
(Query.Find.byId tbl)
$"SELECT data FROM {tbl} WHERE data ->> 'Id' = @id"
"SELECT by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Find.byField tbl "Golf" GE)
$"SELECT data FROM {tbl} WHERE data ->> 'Golf' >= @field"
"SELECT by JSON comparison query not correct"
}
]
testList "Delete" [
test "byId succeeds" {
Expect.equal
(Query.Delete.byId tbl)
$"DELETE FROM {tbl} WHERE data ->> 'Id' = @id"
"DELETE by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Delete.byField tbl "gone" NEX)
$"DELETE FROM {tbl} WHERE data ->> 'gone' IS NULL"
"DELETE by JSON comparison query not correct"
}
]
]
]

View File

@ -0,0 +1,700 @@
module PostgresExtensionTests
open BitBadger.Documents
open BitBadger.Documents.Postgres
open BitBadger.Documents.Tests
open Expecto
open Npgsql
open Types
/// Open a connection to the throwaway database
let private mkConn (db: ThrowawayPostgresDb) =
let conn = new NpgsqlConnection(db.ConnectionString)
conn.Open()
conn
/// Integration tests for the F# extensions on the NpgsqlConnection data type
let integrationTests =
let loadDocs (conn: NpgsqlConnection) = backgroundTask {
for doc in testDocuments do do! conn.insert PostgresDb.TableName doc
}
testList "Postgres.Extensions" [
testList "customList" [
testTask "succeeds when data is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.customList (Query.selectFromTable PostgresDb.TableName) [] fromData<JsonDocument>
Expect.equal (List.length docs) 5 "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs =
conn.customList
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ]
fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "customSingle" [
testTask "succeeds when a row is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.customSingle
$"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "one"]
fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc =
conn.customSingle
$"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "eighty" ]
fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "customNonQuery" [
testTask "succeeds when operating on data" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.customNonQuery $"DELETE FROM {PostgresDb.TableName}" []
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 0 "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.customNonQuery
$"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ]
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should be 5 documents remaining in the table"
}
]
testTask "scalar succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! nbr = conn.customScalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value")
Expect.equal nbr 5 "The query should have returned the number 5"
}
testTask "ensureTable succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let tableExists () =
conn.customScalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists
let keyExists () =
conn.customScalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists
let! exists = tableExists ()
let! alsoExists = keyExists ()
Expect.isFalse exists "The table should not exist already"
Expect.isFalse alsoExists "The key index should not exist already"
do! conn.ensureTable "ensured"
let! exists' = tableExists ()
let! alsoExists' = keyExists ()
Expect.isTrue exists' "The table should now exist"
Expect.isTrue alsoExists' "The key index should now exist"
}
testTask "ensureDocumentIndex succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let indexExists () =
conn.customScalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it" [] toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! conn.ensureTable "ensured"
do! conn.ensureDocumentIndex "ensured" Optimized
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
testTask "ensureFieldIndex succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let indexExists () =
conn.customScalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! conn.ensureTable "ensured"
do! conn.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ]
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
testList "insert" [
testTask "succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } }
do! conn.insert PostgresDb.TableName testDoc
let! after = conn.findAll<JsonDocument> PostgresDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "fails for duplicate key" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! conn.insert PostgresDb.TableName { emptyDoc with Id = "test" }
Expect.throws
(fun () ->
conn.insert PostgresDb.TableName {emptyDoc with Id = "test" }
|> Async.AwaitTask
|> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! conn.save PostgresDb.TableName testDoc
let! after = conn.findAll<JsonDocument> PostgresDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! conn.insert PostgresDb.TableName testDoc
let! before = conn.findById<string, JsonDocument> PostgresDb.TableName "test"
Expect.isSome before "There should have been a document returned"
Expect.equal before.Value testDoc "The document is not correct"
let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } }
do! conn.save PostgresDb.TableName upd8Doc
let! after = conn.findById<string, JsonDocument> PostgresDb.TableName "test"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value upd8Doc "The updated document is not correct"
}
]
testTask "countAll succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! theCount = conn.countAll PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents"
}
testTask "countByField succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! theCount = conn.countByField PostgresDb.TableName "Value" EQ "purple"
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "countByContains succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! theCount = conn.countByContains PostgresDb.TableName {| Value = "purple" |}
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "countByJsonPath succeeds" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! theCount = conn.countByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)"
Expect.equal theCount 3 "There should have been 3 matching documents"
}
testList "existsById" [
testTask "succeeds when a document exists" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsById PostgresDb.TableName "three"
Expect.isTrue exists "There should have been an existing document"
}
testTask "succeeds when a document does not exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsById PostgresDb.TableName "seven"
Expect.isFalse exists "There should not have been an existing document"
}
]
testList "existsByField" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByField PostgresDb.TableName "Sub" EX ""
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByField PostgresDb.TableName "NumValue" EQ "six"
Expect.isFalse exists "There should not have been existing documents"
}
]
testList "existsByContains" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByContains PostgresDb.TableName {| NumValue = 10 |}
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByContains PostgresDb.TableName {| Nothing = "none" |}
Expect.isFalse exists "There should not have been any existing documents"
}
]
testList "existsByJsonPath" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! exists = conn.existsByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)"
Expect.isFalse exists "There should not have been any existing documents"
}
]
testList "findAll" [
testTask "succeeds when there is data" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! conn.insert PostgresDb.TableName { Foo = "one"; Bar = "two" }
do! conn.insert PostgresDb.TableName { Foo = "three"; Bar = "four" }
do! conn.insert PostgresDb.TableName { Foo = "five"; Bar = "six" }
let! results = conn.findAll<SubDocument> PostgresDb.TableName
let expected = [
{ Foo = "one"; Bar = "two" }
{ Foo = "three"; Bar = "four" }
{ Foo = "five"; Bar = "six" }
]
Expect.equal results expected "There should have been 3 documents returned"
}
testTask "succeeds when there is no data" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! results = conn.findAll<SubDocument> PostgresDb.TableName
Expect.equal results [] "There should have been no documents returned"
}
]
testList "findById" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findById<string, JsonDocument> PostgresDb.TableName "two"
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findById<string, JsonDocument> PostgresDb.TableName "three hundred eighty-seven"
Expect.isNone doc "There should not have been a document returned"
}
]
testList "findByField" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByField<JsonDocument> PostgresDb.TableName "Value" EQ "another"
Expect.equal (List.length docs) 1 "There should have been one document returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByField<JsonDocument> PostgresDb.TableName "Value" EQ "mauve"
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "findByContains" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByContains<JsonDocument> PostgresDb.TableName {| Sub = {| Foo = "green" |} |}
Expect.equal (List.length docs) 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByContains<JsonDocument> PostgresDb.TableName {| Value = "mauve" |}
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "findByJsonPath" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByJsonPath<JsonDocument> PostgresDb.TableName "$.NumValue ? (@ < 15)"
Expect.equal (List.length docs) 3 "There should have been 3 documents returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! docs = conn.findByJsonPath<JsonDocument> PostgresDb.TableName "$.NumValue ? (@ < 0)"
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "findFirstByField" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName "Value" EQ "another"
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName "Value" EQ "purple"
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByField<JsonDocument> PostgresDb.TableName "Value" EQ "absent"
Expect.isNone doc "There should not have been a document returned"
}
]
testList "findFirstByContains" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByContains<JsonDocument> PostgresDb.TableName {| Value = "another" |}
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByContains<JsonDocument> PostgresDb.TableName {| Sub = {| Foo = "green" |} |}
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByContains<JsonDocument> PostgresDb.TableName {| Value = "absent" |}
Expect.isNone doc "There should not have been a document returned"
}
]
testList "findFirstByJsonPath" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByJsonPath<JsonDocument> PostgresDb.TableName """$.Value ? (@ == "FIRST!")"""
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByJsonPath<JsonDocument> PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let! doc = conn.findFirstByJsonPath<JsonDocument> PostgresDb.TableName """$.Id ? (@ == "nope")"""
Expect.isNone doc "There should not have been a document returned"
}
]
testList "updateById" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } }
do! conn.updateById PostgresDb.TableName "one" testDoc
let! after = conn.findById<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value testDoc "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.updateById
PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } }
}
]
testList "updateByFunc" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.updateByFunc
PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
let! after = conn.findById<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal
after.Value
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
"The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.updateByFunc
PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
}
]
testList "patchById" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.patchById PostgresDb.TableName "one" {| NumValue = 44 |}
let! after = conn.findById<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value.NumValue 44 "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |}
}
]
testList "patchByField" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.patchByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |}
let! after = conn.countByField PostgresDb.TableName "NumValue" EQ "77"
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |}
}
]
testList "patchByContains" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.patchByContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |}
let! after = conn.countByContains PostgresDb.TableName {| NumValue = 77 |}
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |}
}
]
testList "patchByJsonPath" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.patchByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |}
let! after = conn.countByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)"
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
let! before = conn.countAll PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |}
}
]
testList "deleteById" [
testTask "succeeds when a document is deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteById PostgresDb.TableName "four"
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 4 "There should have been 4 documents remaining"
}
testTask "succeeds when a document is not deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteById PostgresDb.TableName "thirty"
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "deleteByField" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByField PostgresDb.TableName "Value" EQ "purple"
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByField PostgresDb.TableName "Value" EQ "crimson"
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "deleteByContains" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByContains PostgresDb.TableName {| Value = "purple" |}
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByContains PostgresDb.TableName {| Value = "crimson" |}
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "deleteByJsonPath" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
use conn = mkConn db
do! loadDocs conn
do! conn.deleteByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)"
let! remaining = conn.countAll PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
]
|> testSequenced

816
src/Tests/PostgresTests.fs Normal file
View File

@ -0,0 +1,816 @@
module PostgresTests
open Expecto
open BitBadger.Documents
open BitBadger.Documents.Postgres
open BitBadger.Documents.Tests
/// Tests which do not hit the database
let unitTests =
testList "Unit" [
testList "Parameters" [
test "idParam succeeds" {
Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly"
}
test "jsonParam succeeds" {
Expect.equal
(jsonParam "@test" {| Something = "good" |})
("@test", Sql.jsonb """{"Something":"good"}""")
"JSON parameter not constructed correctly"
}
test "fieldParam succeeds" {
let it = fieldParam 242
Expect.equal (fst it) "@field" "Field parameter name not correct"
match snd it with
| SqlValue.Parameter value ->
Expect.equal value.ParameterName "@field" "Parameter name not correct"
Expect.equal value.Value 242 "Parameter value not correct"
| _ -> Expect.isTrue false "The parameter was not a Parameter type"
}
test "noParams succeeds" {
Expect.isEmpty noParams "The no-params sequence should be empty"
}
]
testList "Query" [
testList "Definition" [
test "ensureTable succeeds" {
Expect.equal
(Query.Definition.ensureTable PostgresDb.TableName)
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
test "ensureDocumentIndex succeeds for full index" {
Expect.equal
(Query.Definition.ensureDocumentIndex "schema.tbl" Full)
"CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)"
"CREATE INDEX statement not constructed correctly"
}
test "ensureDocumentIndex succeeds for JSONB Path Ops index" {
Expect.equal
(Query.Definition.ensureDocumentIndex PostgresDb.TableName Optimized)
(sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)"
PostgresDb.TableName PostgresDb.TableName)
"CREATE INDEX statement not constructed correctly"
}
]
test "whereDataContains succeeds" {
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct"
}
test "whereJsonPathMatches succeeds" {
Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
}
testList "Count" [
test "byContains succeeds" {
Expect.equal
(Query.Count.byContains PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria"
"JSON containment count query not correct"
}
test "byJsonPath succeeds" {
Expect.equal
(Query.Count.byJsonPath PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
"JSON Path match count query not correct"
}
]
testList "Exists" [
test "byContains succeeds" {
Expect.equal
(Query.Exists.byContains PostgresDb.TableName)
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it"
"JSON containment exists query not correct"
}
test "byJsonPath succeeds" {
Expect.equal
(Query.Exists.byJsonPath PostgresDb.TableName)
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it"
"JSON Path match existence query not correct"
}
]
testList "Find" [
test "byContains succeeds" {
Expect.equal
(Query.Find.byContains PostgresDb.TableName)
$"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria"
"SELECT by JSON containment query not correct"
}
test "byJsonPath succeeds" {
Expect.equal
(Query.Find.byJsonPath PostgresDb.TableName)
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
"SELECT by JSON Path match query not correct"
}
]
testList "Patch" [
test "byId succeeds" {
Expect.equal
(Query.Patch.byId PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id"
"UPDATE partial by ID statement not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Patch.byField PostgresDb.TableName "Snail" LT)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field"
"UPDATE partial by ID statement not correct"
}
test "byContains succeeds" {
Expect.equal
(Query.Patch.byContains PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria"
"UPDATE partial by JSON containment statement not correct"
}
test "byJsonPath succeeds" {
Expect.equal
(Query.Patch.byJsonPath PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath"
"UPDATE partial by JSON Path statement not correct"
}
]
testList "Delete" [
test "byContains succeeds" {
Expect.equal (Query.Delete.byContains PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria"
"DELETE by JSON containment query not correct"
}
test "byJsonPath succeeds" {
Expect.equal (Query.Delete.byJsonPath PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
"DELETE by JSON Path match query not correct"
}
]
]
]
open ThrowawayDb.Postgres
open Types
let isTrue<'T> (_ : 'T) = true
let integrationTests =
let documents = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]
let loadDocs () = backgroundTask {
for doc in documents do do! insert PostgresDb.TableName doc
}
testList "Integration" [
testList "Configuration" [
test "useDataSource disposes existing source" {
use db1 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value
let source = PostgresDb.MkDataSource db1.ConnectionString
Configuration.useDataSource source
use db2 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value
Configuration.useDataSource (PostgresDb.MkDataSource db2.ConnectionString)
Expect.throws (fun () -> source.OpenConnection() |> ignore) "Data source should have been disposed"
}
test "dataSource returns configured data source" {
use db = ThrowawayDatabase.Create PostgresDb.ConnStr.Value
let source = PostgresDb.MkDataSource db.ConnectionString
Configuration.useDataSource source
Expect.isTrue (obj.ReferenceEquals(source, Configuration.dataSource ()))
"Data source should have been the same"
}
]
testList "Custom" [
testList "list" [
testTask "succeeds when data is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs =
Custom.list $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ] fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "single" [
testTask "succeeds when a row is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "one"] fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "eighty" ] fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "nonQuery" [
testTask "succeeds when operating on data" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" []
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 0 "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ]
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should be 5 documents remaining in the table"
}
]
testTask "scalar succeeds" {
use db = PostgresDb.BuildDb()
let! nbr = Custom.scalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value")
Expect.equal nbr 5 "The query should have returned the number 5"
}
]
testList "Definition" [
testTask "ensureTable succeeds" {
use db = PostgresDb.BuildDb()
let tableExists () =
Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists
let keyExists () =
Custom.scalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists
let! exists = tableExists ()
let! alsoExists = keyExists ()
Expect.isFalse exists "The table should not exist already"
Expect.isFalse alsoExists "The key index should not exist already"
do! Definition.ensureTable "ensured"
let! exists' = tableExists ()
let! alsoExists' = keyExists ()
Expect.isTrue exists' "The table should now exist"
Expect.isTrue alsoExists' "The key index should now exist"
}
testTask "ensureDocumentIndex succeeds" {
use db = PostgresDb.BuildDb()
let indexExists () =
Custom.scalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it"
[]
toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! Definition.ensureTable "ensured"
do! Definition.ensureDocumentIndex "ensured" Optimized
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
testTask "ensureFieldIndex succeeds" {
use db = PostgresDb.BuildDb()
let indexExists () =
Custom.scalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! Definition.ensureTable "ensured"
do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ]
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
]
testList "insert" [
testTask "succeeds" {
use db = PostgresDb.BuildDb()
let! before = Find.all<SubDocument> PostgresDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } }
do! insert PostgresDb.TableName testDoc
let! after = Find.all<JsonDocument> PostgresDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "fails for duplicate key" {
use db = PostgresDb.BuildDb()
do! insert PostgresDb.TableName { emptyDoc with Id = "test" }
Expect.throws
(fun () ->
insert PostgresDb.TableName {emptyDoc with Id = "test" }
|> Async.AwaitTask
|> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
use db = PostgresDb.BuildDb()
let! before = Find.all<JsonDocument> PostgresDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! save PostgresDb.TableName testDoc
let! after = Find.all<JsonDocument> PostgresDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! insert PostgresDb.TableName testDoc
let! before = Find.byId<string, JsonDocument> PostgresDb.TableName "test"
Expect.isSome before "There should have been a document returned"
Expect.equal before.Value testDoc "The document is not correct"
let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } }
do! save PostgresDb.TableName upd8Doc
let! after = Find.byId<string, JsonDocument> PostgresDb.TableName "test"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value upd8Doc "The updated document is not correct"
}
]
testList "Count" [
testTask "all succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents"
}
testTask "byField succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName "Value" EQ "purple"
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "byContains succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byContains PostgresDb.TableName {| Value = "purple" |}
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "byJsonPath succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)"
Expect.equal theCount 3 "There should have been 3 matching documents"
}
]
testList "Exists" [
testList "byId" [
testTask "succeeds when a document exists" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byId PostgresDb.TableName "three"
Expect.isTrue exists "There should have been an existing document"
}
testTask "succeeds when a document does not exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byId PostgresDb.TableName "seven"
Expect.isFalse exists "There should not have been an existing document"
}
]
testList "byField" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField PostgresDb.TableName "Sub" EX ""
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField PostgresDb.TableName "NumValue" EQ "six"
Expect.isFalse exists "There should not have been existing documents"
}
]
testList "byContains" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byContains PostgresDb.TableName {| NumValue = 10 |}
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byContains PostgresDb.TableName {| Nothing = "none" |}
Expect.isFalse exists "There should not have been any existing documents"
}
]
testList "byJsonPath" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)"
Expect.isFalse exists "There should not have been any existing documents"
}
]
]
testList "Find" [
testList "all" [
testTask "succeeds when there is data" {
use db = PostgresDb.BuildDb()
do! insert PostgresDb.TableName { Foo = "one"; Bar = "two" }
do! insert PostgresDb.TableName { Foo = "three"; Bar = "four" }
do! insert PostgresDb.TableName { Foo = "five"; Bar = "six" }
let! results = Find.all<SubDocument> PostgresDb.TableName
let expected = [
{ Foo = "one"; Bar = "two" }
{ Foo = "three"; Bar = "four" }
{ Foo = "five"; Bar = "six" }
]
Expect.equal results expected "There should have been 3 documents returned"
}
testTask "succeeds when there is no data" {
use db = PostgresDb.BuildDb()
let! results = Find.all<SubDocument> PostgresDb.TableName
Expect.equal results [] "There should have been no documents returned"
}
]
testList "byId" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.byId<string, JsonDocument> PostgresDb.TableName "two"
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.byId<string, JsonDocument> PostgresDb.TableName "three hundred eighty-seven"
Expect.isNone doc "There should not have been a document returned"
}
]
testList "byField" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byField<JsonDocument> PostgresDb.TableName "Value" EQ "another"
Expect.equal (List.length docs) 1 "There should have been one document returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byField<JsonDocument> PostgresDb.TableName "Value" EQ "mauve"
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "byContains" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byContains<JsonDocument> PostgresDb.TableName {| Sub = {| Foo = "green" |} |}
Expect.equal (List.length docs) 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byContains<JsonDocument> PostgresDb.TableName {| Value = "mauve" |}
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "byJsonPath" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byJsonPath<JsonDocument> PostgresDb.TableName "$.NumValue ? (@ < 15)"
Expect.equal (List.length docs) 3 "There should have been 3 documents returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Find.byJsonPath<JsonDocument> PostgresDb.TableName "$.NumValue ? (@ < 0)"
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "firstByField" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> PostgresDb.TableName "Value" EQ "another"
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> PostgresDb.TableName "Value" EQ "purple"
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> PostgresDb.TableName "Value" EQ "absent"
Expect.isNone doc "There should not have been a document returned"
}
]
testList "firstByContains" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByContains<JsonDocument> PostgresDb.TableName {| Value = "another" |}
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByContains<JsonDocument> PostgresDb.TableName {| Sub = {| Foo = "green" |} |}
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByContains<JsonDocument> PostgresDb.TableName {| Value = "absent" |}
Expect.isNone doc "There should not have been a document returned"
}
]
testList "firstByJsonPath" [
testTask "succeeds when a document is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByJsonPath<JsonDocument> PostgresDb.TableName """$.Value ? (@ == "FIRST!")"""
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByJsonPath<JsonDocument> PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
Expect.isSome doc "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByJsonPath<JsonDocument> PostgresDb.TableName """$.Id ? (@ == "nope")"""
Expect.isNone doc "There should not have been a document returned"
}
]
]
testList "Update" [
testList "byId" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } }
do! Update.byId PostgresDb.TableName "one" testDoc
let! after = Find.byId<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value testDoc "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Update.byId
PostgresDb.TableName
"test"
{ emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } }
}
]
testList "byFunc" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Update.byFunc PostgresDb.TableName (_.Id)
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
let! after = Find.byId<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal
after.Value
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
"The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Update.byFunc
PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
}
]
]
testList "Patch" [
testList "byId" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Patch.byId PostgresDb.TableName "one" {| NumValue = 44 |}
let! after = Find.byId<string, JsonDocument> PostgresDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value.NumValue 44 "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |}
}
]
testList "byField" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Patch.byField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |}
let! after = Count.byField PostgresDb.TableName "NumValue" EQ "77"
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |}
}
]
testList "byContains" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Patch.byContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |}
let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |}
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |}
}
]
testList "byJsonPath" [
testTask "succeeds when a document is updated" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |}
let! after = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)"
Expect.equal after 2 "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use db = PostgresDb.BuildDb()
let! before = Count.all PostgresDb.TableName
Expect.equal before 0 "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |}
}
]
]
testList "Delete" [
testList "byId" [
testTask "succeeds when a document is deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byId PostgresDb.TableName "four"
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 4 "There should have been 4 documents remaining"
}
testTask "succeeds when a document is not deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byId PostgresDb.TableName "thirty"
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "byField" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byField PostgresDb.TableName "Value" EQ "purple"
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byField PostgresDb.TableName "Value" EQ "crimson"
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "byContains" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byContains PostgresDb.TableName {| Value = "purple" |}
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byContains PostgresDb.TableName {| Value = "crimson" |}
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
testList "byJsonPath" [
testTask "succeeds when documents are deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")"""
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 3 "There should have been 3 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Delete.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)"
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should have been 5 documents remaining"
}
]
]
]
|> testSequenced
let all = testList "Postgres" [ unitTests; integrationTests ]

19
src/Tests/Program.fs Normal file
View File

@ -0,0 +1,19 @@
open Expecto
open BitBadger.Documents.Tests.CSharp
let allTests =
testList
"BitBadger.Documents"
[ CommonTests.all
CommonCSharpTests.Unit
PostgresTests.all
PostgresCSharpTests.All
PostgresExtensionTests.integrationTests
testSequenced PostgresCSharpExtensionTests.Integration
SqliteTests.all
SqliteCSharpTests.All
SqliteExtensionTests.integrationTests
testSequenced SqliteCSharpExtensionTests.Integration ]
[<EntryPoint>]
let main args = runTestsWithCLIArgs [] args allTests

View File

@ -0,0 +1,471 @@
module SqliteExtensionTests
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open BitBadger.Documents.Tests
open Expecto
open Microsoft.Data.Sqlite
open Types
/// Integration tests for the F# extensions on the SqliteConnection data type
let integrationTests =
let loadDocs () = backgroundTask {
for doc in testDocuments do do! insert SqliteDb.TableName doc
}
testList "Sqlite.Extensions" [
testTask "ensureTable succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let itExists (name: string) =
conn.customScalar
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it"
[ SqliteParameter("@name", name) ]
toExists
let! exists = itExists "ensured"
let! alsoExists = itExists "idx_ensured_key"
Expect.isFalse exists "The table should not exist already"
Expect.isFalse alsoExists "The key index should not exist already"
do! conn.ensureTable "ensured"
let! exists' = itExists "ensured"
let! alsoExists' = itExists "idx_ensured_key"
Expect.isTrue exists' "The table should now exist"
Expect.isTrue alsoExists' "The key index should now exist"
}
testTask "ensureFieldIndex succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let indexExists () =
conn.customScalar
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it"
[]
toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! conn.ensureTable "ensured"
do! conn.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ]
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
testList "insert" [
testTask "succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<SubDocument> SqliteDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } }
do! conn.insert SqliteDb.TableName testDoc
let! after = conn.findAll<JsonDocument> SqliteDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "fails for duplicate key" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! conn.insert SqliteDb.TableName { emptyDoc with Id = "test" }
Expect.throws
(fun () ->
conn.insert SqliteDb.TableName {emptyDoc with Id = "test" }
|> Async.AwaitTask
|> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<JsonDocument> SqliteDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! conn.save SqliteDb.TableName testDoc
let! after = conn.findAll<JsonDocument> SqliteDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! conn.insert SqliteDb.TableName testDoc
let! before = conn.findById<string, JsonDocument> SqliteDb.TableName "test"
if Option.isNone before then Expect.isTrue false "There should have been a document returned"
Expect.equal before.Value testDoc "The document is not correct"
let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } }
do! conn.save SqliteDb.TableName upd8Doc
let! after = conn.findById<string, JsonDocument> SqliteDb.TableName "test"
if Option.isNone after then
Expect.isTrue false "There should have been a document returned post-update"
Expect.equal after.Value upd8Doc "The updated document is not correct"
}
]
testTask "countAll succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! theCount = conn.countAll SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents"
}
testTask "countByField succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! theCount = conn.countByField SqliteDb.TableName "Value" EQ "purple"
Expect.equal theCount 2L "There should have been 2 matching documents"
}
testList "existsById" [
testTask "succeeds when a document exists" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsById SqliteDb.TableName "three"
Expect.isTrue exists "There should have been an existing document"
}
testTask "succeeds when a document does not exist" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsById SqliteDb.TableName "seven"
Expect.isFalse exists "There should not have been an existing document"
}
]
testList "existsByField" [
testTask "succeeds when documents exist" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsByField SqliteDb.TableName "NumValue" EQ 10
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! exists = conn.existsByField SqliteDb.TableName "Nothing" EQ "none"
Expect.isFalse exists "There should not have been any existing documents"
}
]
testList "findAll" [
testTask "succeeds when there is data" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" }
do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" }
do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" }
let! results = conn.findAll<SubDocument> SqliteDb.TableName
let expected = [
{ Foo = "one"; Bar = "two" }
{ Foo = "three"; Bar = "four" }
{ Foo = "five"; Bar = "six" }
]
Expect.equal results expected "There should have been 3 documents returned"
}
testTask "succeeds when there is no data" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! results = conn.findAll<SubDocument> SqliteDb.TableName
Expect.equal results [] "There should have been no documents returned"
}
]
testList "findById" [
testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findById<string, JsonDocument> SqliteDb.TableName "two"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
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"
}
]
testList "findByField" [
testTask "succeeds when documents are found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs = conn.findByField<JsonDocument> SqliteDb.TableName "Sub.Foo" EQ "green"
Expect.equal (List.length docs) 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs = conn.findByField<JsonDocument> SqliteDb.TableName "Value" EQ "mauve"
Expect.isTrue (List.isEmpty docs) "There should have been no documents returned"
}
]
testList "findFirstByField" [
testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName "Value" EQ "another"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName "Sub.Foo" EQ "green"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc = conn.findFirstByField<JsonDocument> SqliteDb.TableName "Value" EQ "absent"
Expect.isFalse (Option.isSome doc) "There should not have been a document returned"
}
]
testList "updateById" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } }
do! conn.updateById SqliteDb.TableName "one" testDoc
let! after = conn.findById<string, JsonDocument> SqliteDb.TableName "one"
if Option.isNone after then
Expect.isTrue false "There should have been a document returned post-update"
Expect.equal after.Value testDoc "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<JsonDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! conn.updateById
SqliteDb.TableName
"test"
{ emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } }
}
]
testList "updateByFunc" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.updateByFunc
SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
let! after = conn.findById<string, JsonDocument> SqliteDb.TableName "one"
if Option.isNone after then
Expect.isTrue false "There should have been a document returned post-update"
Expect.equal
after.Value
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
"The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<JsonDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! conn.updateByFunc
SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
}
]
testList "patchById" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.patchById SqliteDb.TableName "one" {| NumValue = 44 |}
let! after = conn.findById<string, JsonDocument> SqliteDb.TableName "one"
if Option.isNone after then
Expect.isTrue false "There should have been a document returned post-update"
Expect.equal after.Value.NumValue 44 "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<SubDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |}
}
]
testList "patchByField" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.patchByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |}
let! after = conn.countByField SqliteDb.TableName "NumValue" EQ 77
Expect.equal after 2L "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! before = conn.findAll<SubDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! conn.patchByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |}
}
]
testList "deleteById" [
testTask "succeeds when a document is deleted" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteById SqliteDb.TableName "four"
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 4L "There should have been 4 documents remaining"
}
testTask "succeeds when a document is not deleted" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteById SqliteDb.TableName "thirty"
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}
]
testList "deleteByField" [
testTask "succeeds when documents are deleted" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteByField SqliteDb.TableName "Value" NE "purple"
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 2L "There should have been 2 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.deleteByField SqliteDb.TableName "Value" EQ "crimson"
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}
]
testList "customSingle" [
testTask "succeeds when a row is found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc =
conn.customSingle
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id"
[ SqliteParameter("@id", "one") ]
fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! doc =
conn.customSingle
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id"
[ SqliteParameter("@id", "eighty") ]
fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "customList" [
testTask "succeeds when data is found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
let! docs =
conn.customList
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value"
[ SqliteParameter("@value", 100) ]
fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "customNonQuery" [
testTask "succeeds when operating on data" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.customNonQuery $"DELETE FROM {SqliteDb.TableName}" []
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 0L "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
do! loadDocs ()
do! conn.customNonQuery
$"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value"
[ SqliteParameter("@value", 100) ]
let! remaining = conn.countAll SqliteDb.TableName
Expect.equal remaining 5L "There should be 5 documents remaining in the table"
}
]
testTask "customScalar succeeds" {
use! db = SqliteDb.BuildDb()
use conn = Configuration.dbConn ()
let! nbr = conn.customScalar "SELECT 5 AS test_value" [] _.GetInt32(0)
Expect.equal nbr 5 "The query should have returned the number 5"
}
test "clean up database" {
Configuration.useConnectionString "data source=:memory:"
}
]
|> testSequenced

536
src/Tests/SqliteTests.fs Normal file
View File

@ -0,0 +1,536 @@
module SqliteTests
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open BitBadger.Documents.Tests
open Expecto
open Microsoft.Data.Sqlite
open Types
/// Unit tests for the SQLite library
let unitTests =
testList "Unit" [
testList "Query" [
test "Definition.ensureTable succeeds" {
Expect.equal
(Query.Definition.ensureTable "tbl")
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)"
"CREATE TABLE statement not correct"
}
testList "Patch" [
test "byId succeeds" {
Expect.equal
(Query.Patch.byId "tbl")
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id"
"UPDATE partial by ID statement not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Patch.byField "tbl" "Part" NE)
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field"
"UPDATE partial by JSON comparison query not correct"
}
]
]
testList "Parameters" [
test "idParam succeeds" {
let theParam = idParam 7
Expect.equal theParam.ParameterName "@id" "The parameter name is incorrect"
Expect.equal theParam.Value "7" "The parameter value is incorrect"
}
test "jsonParam succeeds" {
let theParam = jsonParam "@test" {| Nice = "job" |}
Expect.equal theParam.ParameterName "@test" "The parameter name is incorrect"
Expect.equal theParam.Value """{"Nice":"job"}""" "The parameter value is incorrect"
}
test "fieldParam succeeds" {
let theParam = fieldParam 99
Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect"
Expect.equal theParam.Value 99 "The parameter value is incorrect"
}
test "noParams succeeds" {
Expect.isEmpty noParams "The parameter list should have been empty"
}
]
// Results are exhaustively executed in the context of other tests
]
/// These tests each use a fresh copy of a SQLite database
let integrationTests =
let documents = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]
let loadDocs () = backgroundTask {
for doc in documents do do! insert SqliteDb.TableName doc
}
testList "Integration" [
testList "Configuration" [
test "useConnectionString / connectionString succeed" {
try
Configuration.useConnectionString "Data Source=test.db"
Expect.equal
Configuration.connectionString
(Some "Data Source=test.db;Foreign Keys=True")
"Connection string incorrect"
finally
Configuration.useConnectionString "Data Source=:memory:"
}
test "useSerializer succeeds" {
try
Configuration.useSerializer
{ new IDocumentSerializer with
member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}"""
member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T>
}
let serialized = Configuration.serializer().Serialize { Foo = "howdy"; Bar = "bye"}
Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used"
let deserialized = Configuration.serializer().Deserialize<obj> """{"Something":"here"}"""
Expect.isNull deserialized "Specified serializer should have returned null"
finally
Configuration.useSerializer DocumentSerializer.``default``
}
test "serializer returns configured serializer" {
Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ()))
"Serializer should have been the same"
}
test "useIdField / idField succeeds" {
Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect"
Configuration.useIdField "id"
Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field"
Configuration.useIdField "Id"
}
]
testList "Custom" [
testList "single" [
testTask "succeeds when a row is found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id"
[ SqliteParameter("@id", "one") ]
fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id"
[ SqliteParameter("@id", "eighty") ]
fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "list" [
testTask "succeeds when data is found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Custom.list (Query.selectFromTable SqliteDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs =
Custom.list
$"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value"
[ SqliteParameter("@value", 100) ]
fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "nonQuery" [
testTask "succeeds when operating on data" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" []
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 0L "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery
$"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value"
[ SqliteParameter("@value", 100) ]
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 5L "There should be 5 documents remaining in the table"
}
]
testTask "scalar succeeds" {
use! db = SqliteDb.BuildDb()
let! nbr = Custom.scalar "SELECT 5 AS test_value" [] _.GetInt32(0)
Expect.equal nbr 5 "The query should have returned the number 5"
}
]
testList "Definition" [
testTask "ensureTable succeeds" {
use! db = SqliteDb.BuildDb()
let itExists (name: string) =
Custom.scalar
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it"
[ SqliteParameter("@name", name) ]
toExists
let! exists = itExists "ensured"
let! alsoExists = itExists "idx_ensured_key"
Expect.isFalse exists "The table should not exist already"
Expect.isFalse alsoExists "The key index should not exist already"
do! Definition.ensureTable "ensured"
let! exists' = itExists "ensured"
let! alsoExists' = itExists "idx_ensured_key"
Expect.isTrue exists' "The table should now exist"
Expect.isTrue alsoExists' "The key index should now exist"
}
testTask "ensureFieldIndex succeeds" {
use! db = SqliteDb.BuildDb()
let indexExists () =
Custom.scalar
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it"
[]
toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! Definition.ensureTable "ensured"
do! Definition.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ]
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
}
]
testList "insert" [
testTask "succeeds" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<SubDocument> SqliteDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } }
do! insert SqliteDb.TableName testDoc
let! after = Find.all<JsonDocument> SqliteDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "fails for duplicate key" {
use! db = SqliteDb.BuildDb()
do! insert SqliteDb.TableName { emptyDoc with Id = "test" }
Expect.throws
(fun () ->
insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert"
}
]
testList "save" [
testTask "succeeds when a document is inserted" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<JsonDocument> SqliteDb.TableName
Expect.equal before [] "There should be no documents in the table"
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! save SqliteDb.TableName testDoc
let! after = Find.all<JsonDocument> SqliteDb.TableName
Expect.equal after [ testDoc ] "There should have been one document inserted"
}
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } }
do! insert SqliteDb.TableName testDoc
let! before = Find.byId<string, JsonDocument> SqliteDb.TableName "test"
Expect.isSome before "There should have been a document returned"
Expect.equal before.Value testDoc "The document is not correct"
let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } }
do! save SqliteDb.TableName upd8Doc
let! after = Find.byId<string, JsonDocument> SqliteDb.TableName "test"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value upd8Doc "The updated document is not correct"
}
]
testList "Count" [
testTask "all succeeds" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.all SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents"
}
testTask "byField succeeds" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField SqliteDb.TableName "Value" EQ "purple"
Expect.equal theCount 2L "There should have been 2 matching documents"
}
]
testList "Exists" [
testList "byId" [
testTask "succeeds when a document exists" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byId SqliteDb.TableName "three"
Expect.isTrue exists "There should have been an existing document"
}
testTask "succeeds when a document does not exist" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byId SqliteDb.TableName "seven"
Expect.isFalse exists "There should not have been an existing document"
}
]
testList "byField" [
testTask "succeeds when documents exist" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField SqliteDb.TableName "NumValue" EQ 10
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when no matching documents exist" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField SqliteDb.TableName "Nothing" LT "none"
Expect.isFalse exists "There should not have been any existing documents"
}
]
]
testList "Find" [
testList "all" [
testTask "succeeds when there is data" {
use! db = SqliteDb.BuildDb()
do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" }
do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" }
do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" }
let! results = Find.all<SubDocument> SqliteDb.TableName
let expected = [
{ Foo = "one"; Bar = "two" }
{ Foo = "three"; Bar = "four" }
{ Foo = "five"; Bar = "six" }
]
Expect.equal results expected "There should have been 3 documents returned"
}
testTask "succeeds when there is no data" {
use! db = SqliteDb.BuildDb()
let! results = Find.all<SubDocument> SqliteDb.TableName
Expect.equal results [] "There should have been no documents returned"
}
]
testList "byId" [
testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.byId<string, JsonDocument> SqliteDb.TableName "two"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.byId<string, JsonDocument> SqliteDb.TableName "three hundred eighty-seven"
Expect.isFalse (Option.isSome doc) "There should not have been a document returned"
}
]
testList "byField" [
testTask "succeeds when documents are found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Find.byField<JsonDocument> SqliteDb.TableName "NumValue" GT 15
Expect.equal (List.length docs) 2 "There should have been two documents returned"
}
testTask "succeeds when documents are not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs = Find.byField<JsonDocument> SqliteDb.TableName "NumValue" GT 100
Expect.isTrue (List.isEmpty docs) "There should have been no documents returned"
}
]
testList "firstByField" [
testTask "succeeds when a document is found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> SqliteDb.TableName "Value" EQ "another"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.equal doc.Value.Id "two" "The incorrect document was returned"
}
testTask "succeeds when multiple documents are found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> SqliteDb.TableName "Sub.Foo" EQ "green"
Expect.isTrue (Option.isSome doc) "There should have been a document returned"
Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned"
}
testTask "succeeds when a document is not found" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! doc = Find.firstByField<JsonDocument> SqliteDb.TableName "Value" EQ "absent"
Expect.isFalse (Option.isSome doc) "There should not have been a document returned"
}
]
]
testList "Update" [
testList "byId" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } }
do! Update.byId SqliteDb.TableName "one" testDoc
let! after = Find.byId<string, JsonDocument> SqliteDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value testDoc "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<JsonDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! Update.byId
SqliteDb.TableName
"test"
{ emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } }
}
]
testList "byFunc" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Update.byFunc
SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
let! after = Find.byId<string, JsonDocument> SqliteDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal
after.Value
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
"The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<JsonDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! Update.byFunc
SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
}
]
]
testList "Patch" [
testList "byId" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Patch.byId SqliteDb.TableName "one" {| NumValue = 44 |}
let! after = Find.byId<string, JsonDocument> SqliteDb.TableName "one"
Expect.isSome after "There should have been a document returned post-update"
Expect.equal after.Value.NumValue 44 "The updated document is not correct"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<SubDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |}
}
]
testList "byField" [
testTask "succeeds when a document is updated" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Patch.byField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |}
let! after = Count.byField SqliteDb.TableName "NumValue" EQ 77
Expect.equal after 2L "There should have been 2 documents returned"
}
testTask "succeeds when no document is updated" {
use! db = SqliteDb.BuildDb()
let! before = Find.all<SubDocument> SqliteDb.TableName
Expect.isEmpty before "There should have been no documents returned"
// This not raising an exception is the test
do! Patch.byField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |}
}
]
]
testList "Delete" [
testList "byId" [
testTask "succeeds when a document is deleted" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byId SqliteDb.TableName "four"
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 4L "There should have been 4 documents remaining"
}
testTask "succeeds when a document is not deleted" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byId SqliteDb.TableName "thirty"
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}
]
testList "byField" [
testTask "succeeds when documents are deleted" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byField SqliteDb.TableName "Value" NE "purple"
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 2L "There should have been 2 documents remaining"
}
testTask "succeeds when documents are not deleted" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
do! Delete.byField SqliteDb.TableName "Value" EQ "crimson"
let! remaining = Count.all SqliteDb.TableName
Expect.equal remaining 5L "There should have been 5 documents remaining"
}
]
]
test "clean up database" {
Configuration.useConnectionString "data source=:memory:"
}
]
|> testSequenced
let all = testList "Sqlite" [ unitTests; integrationTests ]

23
src/Tests/Types.fs Normal file
View File

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

BIN
src/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB