Import Postgres F# functions
This commit is contained in:
parent
f2bb1c4aba
commit
d02ea638bc
|
@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.C
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite.Extensions", "Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj", "{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite.Extensions", "Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj", "{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Postgres", "Postgres\BitBadger.Documents.Postgres.fsproj", "{30E73486-9D00-440B-B4AC-5B7AC029AE72}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -48,5 +50,9 @@ Global
|
||||||
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
|
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>3.0.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>3.0.0.0</FileVersion>
|
||||||
<VersionPrefix>1.0.0</VersionPrefix>
|
<VersionPrefix>3.0.0</VersionPrefix>
|
||||||
<VersionSuffix>alpha</VersionSuffix>
|
<VersionSuffix>rc-1</VersionSuffix>
|
||||||
<PackageReleaseNotes>Initial release with F# support</PackageReleaseNotes>
|
<PackageReleaseNotes>Initial release with F# support</PackageReleaseNotes>
|
||||||
<Authors>danieljsummers</Authors>
|
<Authors>danieljsummers</Authors>
|
||||||
<Company>Bit Badger Solutions</Company>
|
<Company>Bit Badger Solutions</Company>
|
||||||
|
|
15
src/Postgres/BitBadger.Documents.Postgres.fsproj
Normal file
15
src/Postgres/BitBadger.Documents.Postgres.fsproj
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Library.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
566
src/Postgres/Library.fs
Normal file
566
src/Postgres/Library.fs
Normal file
|
@ -0,0 +1,566 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
/// Configuration for document handling
|
||||||
|
module Configuration =
|
||||||
|
|
||||||
|
/// The data source to use for query execution
|
||||||
|
let mutable private dataSourceValue : Npgsql.NpgsqlDataSource option = None
|
||||||
|
|
||||||
|
/// Register a data source to use for query execution (disposes the current one if it exists)
|
||||||
|
let useDataSource source =
|
||||||
|
if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose()
|
||||||
|
dataSourceValue <- Some source
|
||||||
|
|
||||||
|
/// Retrieve the currently configured data source
|
||||||
|
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
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
/// Execute a task and ignore the result
|
||||||
|
let internal ignoreTask<'T> (it : Task<'T>) = backgroundTask {
|
||||||
|
let! _ = it
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open BitBadger.Documents
|
||||||
|
|
||||||
|
/// Data definition
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Definition =
|
||||||
|
|
||||||
|
/// SQL statement to create a document table
|
||||||
|
let createTable name =
|
||||||
|
$"CREATE TABLE IF NOT EXISTS %s{name} (data JSONB NOT NULL)"
|
||||||
|
|
||||||
|
/// SQL statement to create a key index for a document table
|
||||||
|
let createKey (name : string) =
|
||||||
|
let tableName = name.Split(".") |> Array.last
|
||||||
|
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{tableName}_key ON {name} ((data ->> '{Configuration.idField ()}'))"
|
||||||
|
|
||||||
|
/// SQL statement to create an index on documents in the specified table
|
||||||
|
let createIndex (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} ON {name} USING GIN (data{extraOps})"
|
||||||
|
|
||||||
|
/// Definitions that take SqlProps as their last parameter
|
||||||
|
module WithProps =
|
||||||
|
|
||||||
|
/// Create a document table
|
||||||
|
let ensureTable name sqlProps = backgroundTask {
|
||||||
|
do! sqlProps |> Sql.query (createTable name) |> Sql.executeNonQueryAsync |> ignoreTask
|
||||||
|
do! sqlProps |> Sql.query (createKey name) |> Sql.executeNonQueryAsync |> ignoreTask
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an index on documents in the specified table
|
||||||
|
let ensureIndex name idxType sqlProps =
|
||||||
|
sqlProps |> Sql.query (createIndex name idxType) |> Sql.executeNonQueryAsync |> ignoreTask
|
||||||
|
|
||||||
|
/// Create a document table
|
||||||
|
let ensureTable name =
|
||||||
|
WithProps.ensureTable name (fromDataSource ())
|
||||||
|
|
||||||
|
let ensureIndex name idxType =
|
||||||
|
WithProps.ensureIndex name idxType (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Query construction functions
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Query =
|
||||||
|
|
||||||
|
/// Create a SELECT clause to retrieve the document data from the given table
|
||||||
|
let selectFromTable tableName =
|
||||||
|
$"SELECT data FROM %s{tableName}"
|
||||||
|
|
||||||
|
/// Create a WHERE clause fragment to implement an ID-based query
|
||||||
|
let whereById paramName =
|
||||||
|
$"data ->> '{Configuration.idField ()}' = %s{paramName}"
|
||||||
|
|
||||||
|
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition
|
||||||
|
let whereDataContains paramName =
|
||||||
|
$"data @> %s{paramName}"
|
||||||
|
|
||||||
|
/// Create a WHERE clause fragment to implement a @? (JSON Path match) condition
|
||||||
|
let whereJsonPathMatches paramName =
|
||||||
|
$"data @? %s{paramName}::jsonpath"
|
||||||
|
|
||||||
|
/// Create a JSONB document parameter
|
||||||
|
let jsonbDocParam (it: obj) =
|
||||||
|
Sql.jsonb (Configuration.serializer().Serialize it)
|
||||||
|
|
||||||
|
/// Create ID and data parameters for a query
|
||||||
|
let docParameters<'T> docId (doc: 'T) =
|
||||||
|
[ "@id", Sql.string docId; "@data", jsonbDocParam doc ]
|
||||||
|
|
||||||
|
/// Query to insert a document
|
||||||
|
let insert tableName =
|
||||||
|
$"INSERT INTO %s{tableName} VALUES (@data)"
|
||||||
|
|
||||||
|
/// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
|
||||||
|
let save tableName =
|
||||||
|
sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data"
|
||||||
|
tableName (Configuration.idField ())
|
||||||
|
|
||||||
|
/// Queries for counting documents
|
||||||
|
module Count =
|
||||||
|
|
||||||
|
/// Query to count all documents in a table
|
||||||
|
let all tableName =
|
||||||
|
$"SELECT COUNT(*) AS it FROM %s{tableName}"
|
||||||
|
|
||||||
|
/// Query to count matching documents using a JSON containment query (@>)
|
||||||
|
let byContains tableName =
|
||||||
|
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}"""
|
||||||
|
|
||||||
|
/// Query to count matching documents using a JSON Path match (@?)
|
||||||
|
let byJsonPath tableName =
|
||||||
|
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}"""
|
||||||
|
|
||||||
|
/// Queries for determining document existence
|
||||||
|
module Exists =
|
||||||
|
|
||||||
|
/// Query to determine if a document exists for the given ID
|
||||||
|
let byId tableName =
|
||||||
|
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
|
||||||
|
|
||||||
|
/// Query to determine if documents exist using a JSON containment query (@>)
|
||||||
|
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 (@?)
|
||||||
|
let byJsonPath tableName =
|
||||||
|
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}) AS it"""
|
||||||
|
|
||||||
|
/// Queries for retrieving documents
|
||||||
|
module Find =
|
||||||
|
|
||||||
|
/// Query to retrieve a document by its ID
|
||||||
|
let byId tableName =
|
||||||
|
$"""{selectFromTable tableName} WHERE {whereById "@id"}"""
|
||||||
|
|
||||||
|
/// Query to retrieve documents using a JSON containment query (@>)
|
||||||
|
let byContains tableName =
|
||||||
|
$"""{selectFromTable tableName} WHERE {whereDataContains "@criteria"}"""
|
||||||
|
|
||||||
|
/// Query to retrieve documents using a JSON Path match (@?)
|
||||||
|
let byJsonPath tableName =
|
||||||
|
$"""{selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}"""
|
||||||
|
|
||||||
|
/// Queries to update documents
|
||||||
|
module Update =
|
||||||
|
|
||||||
|
/// Query to update a document
|
||||||
|
let full tableName =
|
||||||
|
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
|
||||||
|
|
||||||
|
/// Query to update a document
|
||||||
|
let partialById tableName =
|
||||||
|
$"""UPDATE %s{tableName} SET data = data || @data WHERE {whereById "@id"}"""
|
||||||
|
|
||||||
|
/// Query to update partial documents matching a JSON containment query (@>)
|
||||||
|
let partialByContains tableName =
|
||||||
|
$"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}"""
|
||||||
|
|
||||||
|
/// Query to update partial documents matching a JSON containment query (@>)
|
||||||
|
let partialByJsonPath tableName =
|
||||||
|
$"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}"""
|
||||||
|
|
||||||
|
/// Queries to delete documents
|
||||||
|
module Delete =
|
||||||
|
|
||||||
|
/// Query to delete a document by its ID
|
||||||
|
let byId tableName =
|
||||||
|
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
|
||||||
|
|
||||||
|
/// Query to delete documents using a JSON containment query (@>)
|
||||||
|
let byContains tableName =
|
||||||
|
$"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}"""
|
||||||
|
|
||||||
|
/// Query to delete documents using a JSON Path match (@?)
|
||||||
|
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
|
||||||
|
let fromDocument<'T> field (row: RowReader) : 'T =
|
||||||
|
Configuration.serializer().Deserialize<'T>(row.string field)
|
||||||
|
|
||||||
|
/// Create a domain item from a document
|
||||||
|
let fromData<'T> row : 'T =
|
||||||
|
fromDocument "data" row
|
||||||
|
|
||||||
|
|
||||||
|
/// Versions of queries that accept SqlProps as the last parameter
|
||||||
|
module WithProps =
|
||||||
|
|
||||||
|
/// Execute a non-query statement to manipulate a document
|
||||||
|
let private executeNonQuery query (document: 'T) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query query
|
||||||
|
|> Sql.parameters [ "@data", Query.jsonbDocParam document ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Execute a non-query statement to manipulate a document with an ID specified
|
||||||
|
let private executeNonQueryWithId query docId (document: 'T) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query query
|
||||||
|
|> Sql.parameters (Query.docParameters docId document)
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Insert a new document
|
||||||
|
let insert<'T> tableName (document: 'T) sqlProps =
|
||||||
|
executeNonQuery (Query.insert tableName) document sqlProps
|
||||||
|
|
||||||
|
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
|
||||||
|
let save<'T> tableName (document: 'T) sqlProps =
|
||||||
|
executeNonQuery (Query.save tableName) document sqlProps
|
||||||
|
|
||||||
|
/// Commands to count documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Count =
|
||||||
|
|
||||||
|
/// Count all documents in a table
|
||||||
|
let all tableName sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Count.all tableName)
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.int "it")
|
||||||
|
|
||||||
|
/// Count matching documents using a JSON containment query (@>)
|
||||||
|
let byContains tableName (criteria: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Count.byContains tableName)
|
||||||
|
|> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ]
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.int "it")
|
||||||
|
|
||||||
|
/// Count matching documents using a JSON Path match query (@?)
|
||||||
|
let byJsonPath tableName jsonPath sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Count.byJsonPath tableName)
|
||||||
|
|> Sql.parameters [ "@path", Sql.string jsonPath ]
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.int "it")
|
||||||
|
|
||||||
|
/// Commands to determine if documents exist
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Exists =
|
||||||
|
|
||||||
|
/// Determine if a document exists for the given ID
|
||||||
|
let byId tableName docId sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Exists.byId tableName)
|
||||||
|
|> Sql.parameters [ "@id", Sql.string docId ]
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
|
||||||
|
/// Determine if a document exists using a JSON containment query (@>)
|
||||||
|
let byContains tableName (criteria: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Exists.byContains tableName)
|
||||||
|
|> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ]
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
|
||||||
|
/// Determine if a document exists using a JSON Path match query (@?)
|
||||||
|
let byJsonPath tableName jsonPath sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Exists.byJsonPath tableName)
|
||||||
|
|> Sql.parameters [ "@path", Sql.string jsonPath ]
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
|
||||||
|
/// Commands to determine if documents exist
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Find =
|
||||||
|
|
||||||
|
/// Retrieve all documents in the given table
|
||||||
|
let all<'T> tableName sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.selectFromTable tableName)
|
||||||
|
|> Sql.executeAsync fromData<'T>
|
||||||
|
|
||||||
|
/// Retrieve a document by its ID
|
||||||
|
let byId<'T> tableName docId sqlProps = backgroundTask {
|
||||||
|
let! results =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Find.byId tableName)
|
||||||
|
|> Sql.parameters [ "@id", Sql.string docId ]
|
||||||
|
|> Sql.executeAsync fromData<'T>
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a JSON containment query (@>)
|
||||||
|
let byContains<'T> tableName (criteria: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Find.byContains tableName)
|
||||||
|
|> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ]
|
||||||
|
|> Sql.executeAsync fromData<'T>
|
||||||
|
|
||||||
|
/// Execute a JSON Path match query (@?)
|
||||||
|
let byJsonPath<'T> tableName jsonPath sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Find.byJsonPath tableName)
|
||||||
|
|> Sql.parameters [ "@path", Sql.string jsonPath ]
|
||||||
|
|> Sql.executeAsync fromData<'T>
|
||||||
|
|
||||||
|
/// Execute a JSON containment query (@>), returning only the first result
|
||||||
|
let firstByContains<'T> tableName (criteria: obj) sqlProps = backgroundTask {
|
||||||
|
let! results = byContains<'T> tableName criteria sqlProps
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a JSON Path match query (@?), returning only the first result
|
||||||
|
let firstByJsonPath<'T> tableName jsonPath sqlProps = backgroundTask {
|
||||||
|
let! results = byJsonPath<'T> tableName jsonPath sqlProps
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commands to update documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Update =
|
||||||
|
|
||||||
|
/// Update an entire document
|
||||||
|
let full<'T> tableName docId (document: 'T) sqlProps =
|
||||||
|
executeNonQueryWithId (Query.Update.full tableName) docId document sqlProps
|
||||||
|
|
||||||
|
/// Update an entire document
|
||||||
|
let fullFunc<'T> tableName (idFunc: 'T -> string) (document: 'T) sqlProps =
|
||||||
|
full tableName (idFunc document) document sqlProps
|
||||||
|
|
||||||
|
/// Update a partial document
|
||||||
|
let partialById tableName docId (partial: obj) sqlProps =
|
||||||
|
executeNonQueryWithId (Query.Update.partialById tableName) docId partial sqlProps
|
||||||
|
|
||||||
|
/// Update partial documents using a JSON containment query in the WHERE clause (@>)
|
||||||
|
let partialByContains tableName (criteria: obj) (partial: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Update.partialByContains tableName)
|
||||||
|
|> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@criteria", Query.jsonbDocParam criteria ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Update partial documents using a JSON Path match query in the WHERE clause (@?)
|
||||||
|
let partialByJsonPath tableName jsonPath (partial: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Update.partialByJsonPath tableName)
|
||||||
|
|> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@path", Sql.string jsonPath ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Commands to delete documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Delete =
|
||||||
|
|
||||||
|
/// Delete a document by its ID
|
||||||
|
let byId tableName docId sqlProps =
|
||||||
|
executeNonQueryWithId (Query.Delete.byId tableName) docId {||} sqlProps
|
||||||
|
|
||||||
|
/// Delete documents by matching a JSON contains query (@>)
|
||||||
|
let byContains tableName (criteria: obj) sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Delete.byContains tableName)
|
||||||
|
|> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Delete documents by matching a JSON Path match query (@?)
|
||||||
|
let byJsonPath tableName path sqlProps =
|
||||||
|
sqlProps
|
||||||
|
|> Sql.query (Query.Delete.byJsonPath tableName)
|
||||||
|
|> Sql.parameters [ "@path", Sql.string path ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Commands to execute custom SQL queries
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Custom =
|
||||||
|
|
||||||
|
/// Execute a query that returns one or no results
|
||||||
|
let single<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = backgroundTask {
|
||||||
|
let! results =
|
||||||
|
Sql.query query sqlProps
|
||||||
|
|> Sql.parameters parameters
|
||||||
|
|> Sql.executeAsync mapFunc
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a query that returns a list of results
|
||||||
|
let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps =
|
||||||
|
Sql.query query sqlProps
|
||||||
|
|> Sql.parameters parameters
|
||||||
|
|> Sql.executeAsync mapFunc
|
||||||
|
|
||||||
|
/// Execute a query that returns no results
|
||||||
|
let nonQuery query parameters sqlProps =
|
||||||
|
Sql.query query sqlProps
|
||||||
|
|> Sql.parameters (List.ofSeq parameters)
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> ignoreTask
|
||||||
|
|
||||||
|
/// Execute a query that returns a scalar value
|
||||||
|
let scalar<'T when 'T : struct> query parameters (mapFunc: RowReader -> 'T) sqlProps =
|
||||||
|
Sql.query query sqlProps
|
||||||
|
|> Sql.parameters parameters
|
||||||
|
|> Sql.executeRowAsync mapFunc
|
||||||
|
|
||||||
|
|
||||||
|
/// Document writing functions
|
||||||
|
[<AutoOpen>]
|
||||||
|
module Document =
|
||||||
|
/// Insert a new document
|
||||||
|
let insert<'T> tableName (document: 'T) =
|
||||||
|
WithProps.insert tableName document (fromDataSource ())
|
||||||
|
|
||||||
|
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
|
||||||
|
let save<'T> tableName (document: 'T) =
|
||||||
|
WithProps.save<'T> tableName document (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Queries to count documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Count =
|
||||||
|
|
||||||
|
/// Count all documents in a table
|
||||||
|
let all tableName =
|
||||||
|
WithProps.Count.all tableName (fromDataSource ())
|
||||||
|
|
||||||
|
/// Count matching documents using a JSON containment query (@>)
|
||||||
|
let byContains tableName criteria =
|
||||||
|
WithProps.Count.byContains tableName criteria (fromDataSource ())
|
||||||
|
|
||||||
|
/// Count matching documents using a JSON Path match query (@?)
|
||||||
|
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
|
||||||
|
let byId tableName docId =
|
||||||
|
WithProps.Exists.byId tableName docId (fromDataSource ())
|
||||||
|
|
||||||
|
/// Determine if a document exists using a JSON containment query (@>)
|
||||||
|
let byContains tableName criteria =
|
||||||
|
WithProps.Exists.byContains tableName criteria (fromDataSource ())
|
||||||
|
|
||||||
|
/// Determine if a document exists using a JSON Path match query (@?)
|
||||||
|
let byJsonPath tableName jsonPath =
|
||||||
|
WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Commands to retrieve documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Find =
|
||||||
|
|
||||||
|
/// Retrieve all documents in the given table
|
||||||
|
let all<'T> tableName =
|
||||||
|
WithProps.Find.all<'T> tableName (fromDataSource ())
|
||||||
|
|
||||||
|
/// Retrieve a document by its ID
|
||||||
|
let byId<'T> tableName docId =
|
||||||
|
WithProps.Find.byId<'T> tableName docId (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a JSON containment query (@>)
|
||||||
|
let byContains<'T> tableName criteria =
|
||||||
|
WithProps.Find.byContains<'T> tableName criteria (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a JSON Path match query (@?)
|
||||||
|
let byJsonPath<'T> tableName jsonPath =
|
||||||
|
WithProps.Find.byJsonPath<'T> tableName jsonPath (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a JSON containment query (@>), returning only the first result
|
||||||
|
let firstByContains<'T> tableName (criteria: obj) =
|
||||||
|
WithProps.Find.firstByContains<'T> tableName criteria (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a JSON Path match query (@?), returning only the first result
|
||||||
|
let firstByJsonPath<'T> tableName jsonPath =
|
||||||
|
WithProps.Find.firstByJsonPath<'T> tableName jsonPath (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Commands to update documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Update =
|
||||||
|
|
||||||
|
/// Update a full document
|
||||||
|
let full<'T> tableName docId (document: 'T) =
|
||||||
|
WithProps.Update.full<'T> tableName docId document (fromDataSource ())
|
||||||
|
|
||||||
|
/// Update a full document
|
||||||
|
let fullFunc<'T> tableName idFunc (document: 'T) =
|
||||||
|
WithProps.Update.fullFunc<'T> tableName idFunc document (fromDataSource ())
|
||||||
|
|
||||||
|
/// Update a partial document
|
||||||
|
let partialById tableName docId (partial: obj) =
|
||||||
|
WithProps.Update.partialById tableName docId partial (fromDataSource ())
|
||||||
|
|
||||||
|
/// Update partial documents using a JSON containment query in the WHERE clause (@>)
|
||||||
|
let partialByContains tableName (criteria: obj) (partial: obj) =
|
||||||
|
WithProps.Update.partialByContains tableName criteria partial (fromDataSource ())
|
||||||
|
|
||||||
|
/// Update partial documents using a JSON Path match query in the WHERE clause (@?)
|
||||||
|
let partialByJsonPath tableName jsonPath (partial: obj) =
|
||||||
|
WithProps.Update.partialByJsonPath tableName jsonPath partial (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Commands to delete documents
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Delete =
|
||||||
|
|
||||||
|
/// Delete a document by its ID
|
||||||
|
let byId tableName docId =
|
||||||
|
WithProps.Delete.byId tableName docId (fromDataSource ())
|
||||||
|
|
||||||
|
/// Delete documents by matching a JSON contains query (@>)
|
||||||
|
let byContains tableName (criteria: obj) =
|
||||||
|
WithProps.Delete.byContains tableName criteria (fromDataSource ())
|
||||||
|
|
||||||
|
/// Delete documents by matching a JSON Path match query (@?)
|
||||||
|
let byJsonPath tableName path =
|
||||||
|
WithProps.Delete.byJsonPath tableName path (fromDataSource ())
|
||||||
|
|
||||||
|
|
||||||
|
/// Commands to execute custom SQL queries
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Custom =
|
||||||
|
|
||||||
|
/// Execute a query that returns one or no results
|
||||||
|
let single<'T> query parameters (mapFunc: RowReader -> 'T) =
|
||||||
|
WithProps.Custom.single query parameters mapFunc (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a query that returns a list of results
|
||||||
|
let list<'T> query parameters (mapFunc: RowReader -> 'T) =
|
||||||
|
WithProps.Custom.list query parameters mapFunc (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a query that returns no results
|
||||||
|
let nonQuery query parameters =
|
||||||
|
WithProps.Custom.nonQuery query parameters (fromDataSource ())
|
||||||
|
|
||||||
|
/// Execute a query that returns a scalar value
|
||||||
|
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) =
|
||||||
|
WithProps.Custom.scalar query parameters mapFunc (fromDataSource ())
|
|
@ -7,12 +7,14 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
|
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
|
||||||
|
<ProjectReference Include="..\Postgres\BitBadger.Documents.Postgres.fsproj" />
|
||||||
<ProjectReference Include="..\Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj" />
|
<ProjectReference Include="..\Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj" />
|
||||||
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
|
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Expecto" Version="10.1.0" />
|
<PackageReference Include="Expecto" Version="10.1.0" />
|
||||||
|
<PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
144
src/Tests.CSharp/PostgresDb.cs
Normal file
144
src/Tests.CSharp/PostgresDb.cs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
using BitBadger.Documents.Postgres;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public string ConnectionString => _db.ConnectionString;
|
||||||
|
|
||||||
|
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(Definition.createTable(TableName), sqlProps));
|
||||||
|
Sql.executeNonQuery(Sql.query(Definition.createKey(TableName), sqlProps));
|
||||||
|
|
||||||
|
Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString));
|
||||||
|
|
||||||
|
return new ThrowawayPostgresDb(database);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="CommonTests.fs" />
|
<Compile Include="CommonTests.fs" />
|
||||||
<Compile Include="Types.fs" />
|
<Compile Include="Types.fs" />
|
||||||
|
<Compile Include="PostgresTests.fs" />
|
||||||
<Compile Include="SqliteTests.fs" />
|
<Compile Include="SqliteTests.fs" />
|
||||||
<Compile Include="SqliteExtensionTests.fs" />
|
<Compile Include="SqliteExtensionTests.fs" />
|
||||||
<Compile Include="Program.fs" />
|
<Compile Include="Program.fs" />
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
|
<ProjectReference Include="..\Common\BitBadger.Documents.Common.fsproj" />
|
||||||
|
<ProjectReference Include="..\Postgres\BitBadger.Documents.Postgres.fsproj" />
|
||||||
<ProjectReference Include="..\Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj" />
|
<ProjectReference Include="..\Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj" />
|
||||||
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
|
<ProjectReference Include="..\Sqlite\BitBadger.Documents.Sqlite.fsproj" />
|
||||||
<ProjectReference Include="..\Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj" />
|
<ProjectReference Include="..\Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj" />
|
||||||
|
|
711
src/Tests/PostgresTests.fs
Normal file
711
src/Tests/PostgresTests.fs
Normal file
|
@ -0,0 +1,711 @@
|
||||||
|
module PostgresTests
|
||||||
|
|
||||||
|
open Expecto
|
||||||
|
open BitBadger.Documents.Postgres
|
||||||
|
open BitBadger.Documents.Tests
|
||||||
|
|
||||||
|
/// Tests which do not hit the database
|
||||||
|
let unitTests =
|
||||||
|
testList "Unit" [
|
||||||
|
testList "Definition" [
|
||||||
|
test "createTable succeeds" {
|
||||||
|
Expect.equal (Definition.createTable PostgresDb.TableName)
|
||||||
|
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)"
|
||||||
|
"CREATE TABLE statement not constructed correctly"
|
||||||
|
}
|
||||||
|
test "createKey succeeds" {
|
||||||
|
Expect.equal (Definition.createKey PostgresDb.TableName)
|
||||||
|
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{PostgresDb.TableName}_key ON {PostgresDb.TableName} ((data ->> 'Id'))"
|
||||||
|
"CREATE INDEX for key statement not constructed correctly"
|
||||||
|
}
|
||||||
|
test "createIndex succeeds for full index" {
|
||||||
|
Expect.equal (Definition.createIndex "schema.tbl" Full)
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_tbl ON schema.tbl USING GIN (data)"
|
||||||
|
"CREATE INDEX statement not constructed correctly"
|
||||||
|
}
|
||||||
|
test "createIndex succeeds for JSONB Path Ops index" {
|
||||||
|
Expect.equal (Definition.createIndex PostgresDb.TableName Optimized)
|
||||||
|
$"CREATE INDEX IF NOT EXISTS idx_{PostgresDb.TableName} ON {PostgresDb.TableName} USING GIN (data jsonb_path_ops)"
|
||||||
|
"CREATE INDEX statement not constructed correctly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "Query" [
|
||||||
|
test "selectFromTable succeeds" {
|
||||||
|
Expect.equal (Query.selectFromTable PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName}"
|
||||||
|
"SELECT statement not correct"
|
||||||
|
}
|
||||||
|
test "whereById succeeds" {
|
||||||
|
Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct"
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
test "jsonbDocParam succeeds" {
|
||||||
|
Expect.equal (Query.jsonbDocParam {| Hello = "There" |}) (Sql.jsonb "{\"Hello\":\"There\"}")
|
||||||
|
"JSONB document not serialized correctly"
|
||||||
|
}
|
||||||
|
test "docParameters succeeds" {
|
||||||
|
let parameters = Query.docParameters "abc123" {| Testing = 456 |}
|
||||||
|
let expected = [
|
||||||
|
"@id", Sql.string "abc123"
|
||||||
|
"@data", Sql.jsonb "{\"Testing\":456}"
|
||||||
|
]
|
||||||
|
Expect.equal parameters expected "There should have been 2 parameters, one string and one JSONB"
|
||||||
|
}
|
||||||
|
test "insert succeeds" {
|
||||||
|
Expect.equal (Query.insert PostgresDb.TableName) $"INSERT INTO {PostgresDb.TableName} VALUES (@data)"
|
||||||
|
"INSERT statement not correct"
|
||||||
|
}
|
||||||
|
test "save succeeds" {
|
||||||
|
Expect.equal (Query.save PostgresDb.TableName)
|
||||||
|
$"INSERT INTO {PostgresDb.TableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data"
|
||||||
|
"INSERT ON CONFLICT UPDATE statement not correct"
|
||||||
|
}
|
||||||
|
testList "Count" [
|
||||||
|
test "all succeeds" {
|
||||||
|
Expect.equal (Query.Count.all PostgresDb.TableName) $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}"
|
||||||
|
"Count query not correct"
|
||||||
|
}
|
||||||
|
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 "byId succeeds" {
|
||||||
|
Expect.equal (Query.Exists.byId PostgresDb.TableName)
|
||||||
|
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id) AS it"
|
||||||
|
"ID existence query not correct"
|
||||||
|
}
|
||||||
|
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 "byId succeeds" {
|
||||||
|
Expect.equal (Query.Find.byId PostgresDb.TableName)
|
||||||
|
$"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" "SELECT by ID query not correct"
|
||||||
|
}
|
||||||
|
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 "Update" [
|
||||||
|
test "full succeeds" {
|
||||||
|
Expect.equal (Query.Update.full PostgresDb.TableName)
|
||||||
|
$"UPDATE {PostgresDb.TableName} SET data = @data WHERE data ->> 'Id' = @id"
|
||||||
|
"UPDATE full statement not correct"
|
||||||
|
}
|
||||||
|
test "partialById succeeds" {
|
||||||
|
Expect.equal (Query.Update.partialById PostgresDb.TableName)
|
||||||
|
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id"
|
||||||
|
"UPDATE partial by ID statement not correct"
|
||||||
|
}
|
||||||
|
test "partialByContains succeeds" {
|
||||||
|
Expect.equal (Query.Update.partialByContains PostgresDb.TableName)
|
||||||
|
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria"
|
||||||
|
"UPDATE partial by JSON containment statement not correct"
|
||||||
|
}
|
||||||
|
test "partialByJsonPath succeeds" {
|
||||||
|
Expect.equal (Query.Update.partialByJsonPath PostgresDb.TableName)
|
||||||
|
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath"
|
||||||
|
"UPDATE partial by JSON Path statement not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "Delete" [
|
||||||
|
test "byId succeeds" {
|
||||||
|
Expect.equal (Query.Delete.byId PostgresDb.TableName) $"DELETE FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
|
||||||
|
"DELETE by ID query not correct"
|
||||||
|
}
|
||||||
|
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 Npgsql.FSharp
|
||||||
|
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 "Definition" [
|
||||||
|
testTask "ensureTable succeeds" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
let tableExists () =
|
||||||
|
Sql.connect db.ConnectionString
|
||||||
|
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it"
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
let keyExists () =
|
||||||
|
Sql.connect db.ConnectionString
|
||||||
|
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it"
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
|
||||||
|
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 "ensureIndex succeeds" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
let indexExists () =
|
||||||
|
Sql.connect db.ConnectionString
|
||||||
|
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured') AS it"
|
||||||
|
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||||
|
|
||||||
|
let! exists = indexExists ()
|
||||||
|
Expect.isFalse exists "The index should not exist already"
|
||||||
|
|
||||||
|
do! Definition.ensureTable "ensured"
|
||||||
|
do! Definition.ensureIndex "ensured" Optimized
|
||||||
|
let! exists' = indexExists ()
|
||||||
|
Expect.isTrue exists' "The index should now exist"
|
||||||
|
// TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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<JsonDocument> PostgresDb.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! save PostgresDb.TableName upd8Doc
|
||||||
|
let! after = Find.byId<JsonDocument> PostgresDb.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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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 "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 "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<JsonDocument> PostgresDb.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 = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
let! doc = Find.byId<JsonDocument> PostgresDb.TableName "three hundred eighty-seven"
|
||||||
|
Expect.isFalse (Option.isSome doc) "There should not have been a document 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.isTrue (List.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.isTrue (List.isEmpty docs) "There should have been no documents 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.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 = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
let! doc = Find.firstByContains<JsonDocument> PostgresDb.TableName {| Sub = {| Foo = "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 = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
let! doc = Find.firstByContains<JsonDocument> PostgresDb.TableName {| Value = "absent" |}
|
||||||
|
Expect.isFalse (Option.isSome 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.isTrue (Option.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.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 = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
let! doc = Find.firstByJsonPath<JsonDocument> PostgresDb.TableName """$.Id ? (@ == "nope")"""
|
||||||
|
Expect.isFalse (Option.isSome doc) "There should not have been a document returned"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
testList "Update" [
|
||||||
|
testList "full" [
|
||||||
|
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.full PostgresDb.TableName "one" testDoc
|
||||||
|
let! after = Find.byId<JsonDocument> PostgresDb.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 = PostgresDb.BuildDb()
|
||||||
|
|
||||||
|
let! before = Find.all<JsonDocument> PostgresDb.TableName
|
||||||
|
Expect.hasCountOf before 0u isTrue "There should have been no documents returned"
|
||||||
|
|
||||||
|
// This not raising an exception is the test
|
||||||
|
do! Update.full PostgresDb.TableName "test"
|
||||||
|
{ emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "fullFunc" [
|
||||||
|
testTask "succeeds when a document is updated" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
do! Update.fullFunc PostgresDb.TableName (_.Id)
|
||||||
|
{ Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
|
||||||
|
let! after = Find.byId<JsonDocument> PostgresDb.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 = PostgresDb.BuildDb()
|
||||||
|
|
||||||
|
let! before = Find.all<JsonDocument> PostgresDb.TableName
|
||||||
|
Expect.hasCountOf before 0u isTrue "There should have been no documents returned"
|
||||||
|
|
||||||
|
// This not raising an exception is the test
|
||||||
|
do! Update.fullFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "partialById" [
|
||||||
|
testTask "succeeds when a document is updated" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
do! Update.partialById PostgresDb.TableName "one" {| NumValue = 44 |}
|
||||||
|
let! after = Find.byId<JsonDocument> PostgresDb.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 = PostgresDb.BuildDb()
|
||||||
|
|
||||||
|
let! before = Find.all<SubDocument> PostgresDb.TableName
|
||||||
|
Expect.hasCountOf before 0u isTrue "There should have been no documents returned"
|
||||||
|
|
||||||
|
// This not raising an exception is the test
|
||||||
|
do! Update.partialById PostgresDb.TableName "test" {| Foo = "green" |}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "partialByContains" [
|
||||||
|
testTask "succeeds when a document is updated" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
do! Update.partialByContains 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 = Find.all<SubDocument> PostgresDb.TableName
|
||||||
|
Expect.hasCountOf before 0u isTrue "There should have been no documents returned"
|
||||||
|
|
||||||
|
// This not raising an exception is the test
|
||||||
|
do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "partialByJsonPath" [
|
||||||
|
testTask "succeeds when a document is updated" {
|
||||||
|
use db = PostgresDb.BuildDb()
|
||||||
|
do! loadDocs ()
|
||||||
|
|
||||||
|
do! Update.partialByJsonPath 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 = Find.all<SubDocument> PostgresDb.TableName
|
||||||
|
Expect.hasCountOf before 0u isTrue "There should have been no documents returned"
|
||||||
|
|
||||||
|
// This not raising an exception is the test
|
||||||
|
do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| 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 "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
testList "Custom" [
|
||||||
|
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 "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 "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|> testSequenced
|
||||||
|
|
||||||
|
|
||||||
|
let all = testList "FSharp.Documents" [ unitTests; integrationTests ]
|
Loading…
Reference in New Issue
Block a user