30 Commits

Author SHA1 Message Date
0c308c5f33 Add ToString for FieldMatch; add tests 2024-08-11 17:05:58 -04:00
d9d37f110d Add tests for PG parameter types
- Alter whereById to expect a parameter
2024-08-10 19:51:13 -04:00
e0fb793ec9 WIP on Postgres type derivation 2024-08-10 15:07:59 -04:00
74e5b77edb Complete common migration, WIP on tests 2024-08-10 09:56:59 -04:00
98bc83ac17 Pull count, exists, update to common 2024-08-09 22:55:12 -04:00
35755df99a WIP on pulling code up to Common 2024-08-09 20:15:08 -04:00
b1c3991e11 WIP on supporting numeric fields 2024-08-09 18:42:10 -04:00
85750e19f2 Add Postgres Query byFields tests 2024-08-08 22:36:48 -04:00
d8f64417e5 Add byFields to SQLite implementation
- Add fieldNameParams/FieldNames to Postgres
(syncs names between both implementations)
2024-08-08 19:45:25 -04:00
202fca272e Prefer seq over list for parameters
- This greatly reduces the duplicated functions needed
between F# and C#; F# lists satisfy the sequence reqs
2024-08-08 17:32:44 -04:00
8a15bdce2e Add byFields to PostgreSQL impl
- Swap howMatched and fields throughout
- Existing tests pass; still need new ones for byFields
2024-08-07 23:23:28 -04:00
d131eda56e Remove internal calls to deprecated funcs 2024-08-07 20:20:10 -04:00
e2232e91bb Add whereByFields code / tests
- Add wrapper class for unnamed field parameters
- Support table qualifiers by field
- Support dot access to document fields/sub-fields
2024-08-07 16:39:15 -04:00
e96c449324 WIP on PG Query.whereByFIelds 2024-08-07 08:30:28 -04:00
433302d995 WIP on field enhancements 2024-08-04 18:59:32 -04:00
039761fcca Remove project-specific release notes 2024-06-05 22:10:48 -04:00
a0da5f83b1 Merge pull request 'Changes for 3.1' (#5) from 3.1 into main
Reviewed-on: #5
2024-06-06 01:45:35 +00:00
d7c8cae0c7 Add tests for alpha BT values
- Update deps
- Update release notes for v3.1
2024-06-05 21:43:03 -04:00
1707d3ce63 First cut of BT operator (#3) 2024-06-05 17:31:33 -04:00
7d7214e9f2 Remove .NET 7 (#4)
- Bump version to 3.1
2024-06-05 07:52:36 -04:00
fc045d021c Bump version to 3
- Add description to each project
- Add .sh files to test and package
2024-04-20 22:58:41 -04:00
aa14333604 Modify test env var handling
Some checks failed
CI / build-and-test (12) (push) Failing after 33s
CI / build-and-test (13) (push) Failing after 42s
CI / build-and-test (14) (push) Failing after 37s
CI / build-and-test (15) (push) Failing after 31s
CI / build-and-test (latest) (push) Failing after 3m8s
CI / publish (push) Has been skipped
2024-04-20 21:18:15 -04:00
72c11f77e5 Set up all 3 .NET versions
Some checks failed
CI / build-and-test (12) (push) Failing after 1m58s
CI / build-and-test (14) (push) Has been cancelled
CI / build-and-test (15) (push) Has been cancelled
CI / build-and-test (latest) (push) Has been cancelled
CI / publish (push) Has been cancelled
CI / build-and-test (13) (push) Has been cancelled
2024-04-20 20:57:49 -04:00
caf34e707c Remove matrix for .NET versions
Some checks failed
CI / build-and-test (12) (push) Failing after 1m48s
CI / build-and-test (14) (push) Has been cancelled
CI / build-and-test (15) (push) Has been cancelled
CI / build-and-test (latest) (push) Has been cancelled
CI / publish (push) Has been cancelled
CI / build-and-test (13) (push) Has been cancelled
2024-04-20 20:21:13 -04:00
b96775f1ed Modify port mapping
Some checks failed
CI / build-and-test (8.0, 13) (push) Waiting to run
CI / build-and-test (6.0, 12) (push) Failing after 39s
CI / build-and-test (6.0, 13) (push) Failing after 24s
CI / build-and-test (6.0, 14) (push) Failing after 23s
CI / build-and-test (6.0, 15) (push) Failing after 25s
CI / build-and-test (6.0, latest) (push) Failing after 24s
CI / build-and-test (7.0, 12) (push) Failing after 26s
CI / build-and-test (7.0, 13) (push) Failing after 25s
CI / build-and-test (7.0, 14) (push) Failing after 25s
CI / build-and-test (7.0, 15) (push) Failing after 26s
CI / build-and-test (7.0, latest) (push) Failing after 25s
CI / build-and-test (8.0, 12) (push) Failing after 39s
CI / build-and-test (8.0, 14) (push) Has been cancelled
CI / build-and-test (8.0, 15) (push) Has been cancelled
CI / build-and-test (8.0, latest) (push) Has been cancelled
CI / publish (push) Has been cancelled
2024-04-20 20:06:13 -04:00
91e84a5059 Move PostgreSQL port 2024-04-20 20:04:33 -04:00
8b1c56d310 Modify CI network name
Some checks failed
CI / build-and-test (6.0, 12) (push) Failing after 0s
CI / build-and-test (6.0, 13) (push) Failing after 0s
CI / build-and-test (6.0, 14) (push) Failing after 1s
CI / build-and-test (6.0, 15) (push) Failing after 1s
CI / build-and-test (6.0, latest) (push) Failing after 0s
CI / build-and-test (7.0, 12) (push) Failing after 0s
CI / build-and-test (7.0, 13) (push) Failing after 0s
CI / build-and-test (7.0, 14) (push) Failing after 0s
CI / build-and-test (7.0, 15) (push) Failing after 1s
CI / build-and-test (7.0, latest) (push) Failing after 0s
CI / build-and-test (8.0, 12) (push) Failing after 0s
CI / build-and-test (8.0, 13) (push) Failing after 0s
CI / build-and-test (8.0, 14) (push) Failing after 1s
CI / build-and-test (8.0, 15) (push) Failing after 0s
CI / build-and-test (8.0, latest) (push) Failing after 1s
CI / publish (push) Has been skipped
2024-04-20 19:55:51 -04:00
6a2b52475f Specify CI network
Some checks failed
CI / build-and-test (6.0, 12) (push) Failing after 0s
CI / build-and-test (6.0, 13) (push) Failing after 0s
CI / build-and-test (6.0, 14) (push) Failing after 0s
CI / build-and-test (6.0, 15) (push) Failing after 0s
CI / build-and-test (6.0, latest) (push) Failing after 0s
CI / build-and-test (7.0, 12) (push) Failing after 0s
CI / build-and-test (7.0, 13) (push) Failing after 0s
CI / build-and-test (7.0, 14) (push) Failing after 0s
CI / build-and-test (7.0, 15) (push) Failing after 0s
CI / build-and-test (7.0, latest) (push) Failing after 1s
CI / build-and-test (8.0, 12) (push) Failing after 0s
CI / build-and-test (8.0, 13) (push) Failing after 0s
CI / build-and-test (8.0, 14) (push) Failing after 0s
CI / build-and-test (8.0, 15) (push) Failing after 0s
CI / build-and-test (8.0, latest) (push) Failing after 0s
CI / publish (push) Has been skipped
2024-04-20 19:46:47 -04:00
ddc7429dc5 Skip restore for Gitea CI
Some checks failed
CI / build-and-test (6.0, 12) (push) Failing after 1s
CI / build-and-test (6.0, 13) (push) Failing after 1s
CI / build-and-test (6.0, 14) (push) Failing after 1s
CI / build-and-test (6.0, 15) (push) Failing after 1s
CI / build-and-test (6.0, latest) (push) Failing after 1s
CI / build-and-test (7.0, 12) (push) Failing after 1s
CI / build-and-test (7.0, 13) (push) Failing after 1s
CI / build-and-test (7.0, 14) (push) Failing after 1s
CI / build-and-test (7.0, 15) (push) Failing after 1s
CI / build-and-test (7.0, latest) (push) Failing after 1s
CI / build-and-test (8.0, 12) (push) Failing after 1s
CI / build-and-test (8.0, 13) (push) Failing after 1s
CI / build-and-test (8.0, 14) (push) Failing after 1s
CI / build-and-test (8.0, 15) (push) Failing after 1s
CI / build-and-test (8.0, latest) (push) Failing after 1s
CI / publish (push) Has been skipped
2024-04-20 19:41:58 -04:00
3c80c46c91 Rename CI for Gitea
Some checks failed
CI / build-and-test (6.0, 12) (push) Failing after 35s
CI / build-and-test (6.0, 13) (push) Failing after 21s
CI / build-and-test (6.0, 14) (push) Failing after 20s
CI / build-and-test (6.0, 15) (push) Failing after 19s
CI / build-and-test (6.0, latest) (push) Failing after 20s
CI / build-and-test (7.0, 12) (push) Failing after 23s
CI / build-and-test (7.0, 13) (push) Failing after 23s
CI / build-and-test (7.0, 14) (push) Failing after 23s
CI / build-and-test (7.0, 15) (push) Failing after 23s
CI / build-and-test (7.0, latest) (push) Failing after 26s
CI / build-and-test (8.0, 12) (push) Failing after 1m27s
CI / build-and-test (8.0, 13) (push) Failing after 1m29s
CI / build-and-test (8.0, 14) (push) Failing after 1m26s
CI / build-and-test (8.0, 15) (push) Failing after 1m26s
CI / build-and-test (8.0, latest) (push) Failing after 1m24s
CI / publish (push) Has been skipped
2024-04-20 16:19:56 -04:00
28 changed files with 2579 additions and 1509 deletions

View File

@@ -6,6 +6,9 @@ on:
pull_request:
branches: [ "main" ]
env:
BBDOX__PG__PORT: 8301
jobs:
build-and-test:
@@ -13,12 +16,13 @@ jobs:
strategy:
matrix:
dotnet-version: [ "6.0", "7.0", "8.0" ]
postgres-version: [ "12", "13", "14", "15", "latest" ]
services:
postgres:
image: postgres:${{ matrix.postgres-version }}
networks:
- runner_overlay
env:
POSTGRES_PASSWORD: postgres
options: >-
@@ -27,20 +31,32 @@ jobs:
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
- "8301:5432"
steps:
- uses: actions/checkout@v3
- name: Setup .NET ${{ matrix.dotnet-version }}.x
- name: Setup .NET 6
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet-version }}.x
dotnet-version: "6.0.x"
- name: Setup .NET 7
uses: actions/setup-dotnet@v3
with:
dotnet-version: "7.0.x"
- name: Setup .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: "8.0.x"
- name: Restore dependencies
run: dotnet restore src/BitBadger.Documents.sln
- name: Build
run: dotnet build src/BitBadger.Documents.sln --no-restore -f net${{ matrix.dotnet-version }}
- name: Test ${{ matrix.dotnet-version }} against PostgreSQL ${{ matrix.postgres-version }}
run: dotnet run --project src/Tests/BitBadger.Documents.Tests.fsproj -f net${{ matrix.dotnet-version }}
run: dotnet build src/BitBadger.Documents.sln --no-restore
- name: Test .NET 6 against PostgreSQL ${{ matrix.postgres-version }}
run: dotnet run --project src/Tests/BitBadger.Documents.Tests.fsproj -f net6.0
- name: Test .NET 7 against PostgreSQL ${{ matrix.postgres-version }}
run: dotnet run --project src/Tests/BitBadger.Documents.Tests.fsproj -f net7.0
- name: Test .NET 8 against PostgreSQL ${{ matrix.postgres-version }}
run: dotnet run --project src/Tests/BitBadger.Documents.Tests.fsproj -f net8.0
publish:
runs-on: ubuntu-latest
needs: build-and-test

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Added Field type for by-field operations</PackageReleaseNotes>
<Description>Common files for PostgreSQL and SQLite document database libraries</Description>
<PackageTags>JSON Document SQL</PackageTags>
</PropertyGroup>
@@ -12,7 +12,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.2.42" />
<PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

View File

@@ -15,6 +15,8 @@ type Op =
| LE
/// Not Equal to (<>)
| NE
/// Between (BETWEEN)
| BT
/// Exists (IS NOT NULL)
| EX
/// Does Not Exist (IS NULL)
@@ -28,10 +30,17 @@ type Op =
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
/// The dialect in which a command should be rendered
[<Struct>]
type Dialect =
| PostgreSQL
| SQLite
/// Criteria for a field WHERE clause
type Field = {
/// The name of the field
@@ -42,39 +51,98 @@ type Field = {
/// The value of the field
Value: obj
/// The name of the parameter for this field
ParameterName: string option
/// The table qualifier for this field
Qualifier: string option
} with
/// Create an equals (=) field criterion
static member EQ name (value: obj) =
{ Name = name; Op = EQ; Value = value }
{ Name = name; Op = EQ; Value = value; ParameterName = None; Qualifier = None }
/// Create a greater than (>) field criterion
static member GT name (value: obj) =
{ Name = name; Op = GT; Value = value }
{ Name = name; Op = GT; Value = value; ParameterName = None; Qualifier = None }
/// Create a greater than or equal to (>=) field criterion
static member GE name (value: obj) =
{ Name = name; Op = GE; Value = value }
{ Name = name; Op = GE; Value = value; ParameterName = None; Qualifier = None }
/// Create a less than (<) field criterion
static member LT name (value: obj) =
{ Name = name; Op = LT; Value = value }
{ Name = name; Op = LT; Value = value; ParameterName = None; Qualifier = None }
/// Create a less than or equal to (<=) field criterion
static member LE name (value: obj) =
{ Name = name; Op = LE; Value = value }
{ Name = name; Op = LE; Value = value; ParameterName = None; Qualifier = None }
/// Create a not equals (<>) field criterion
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value }
{ Name = name; Op = NE; Value = value; ParameterName = None; Qualifier = None }
/// Create a BETWEEN field criterion
static member BT name (min: obj) (max: obj) =
{ Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None }
/// Create an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj () }
{ Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None }
/// Create an not exists (IS NULL) field criterion
/// Create a not exists (IS NULL) field criterion
static member NEX name =
{ Name = name; Op = NEX; Value = obj () }
{ Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None }
/// Transform a field name (a.b.c) to a path for the given SQL dialect
static member NameToPath (name: string) dialect =
let path =
if name.Contains '.' then
match dialect with
| PostgreSQL -> "#>>'{" + String.concat "," (name.Split '.') + "}'"
| SQLite -> "->>'" + String.concat "'->>'" (name.Split '.') + "'"
else $"->>'{name}'"
$"data{path}"
/// Specify the name of the parameter for this field
member this.WithParameterName name =
{ this with ParameterName = Some name }
/// Specify a qualifier (alias) for the table from which this field will be referenced
member this.WithQualifier alias =
{ this with Qualifier = Some alias }
/// Get the qualified path to the field
member this.Path dialect =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect
/// How fields should be matched
[<Struct>]
type FieldMatch =
/// Any field matches (OR)
| Any
/// All fields match (AND)
| All
/// The SQL value implementing each matching strategy
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
type ParameterName() =
/// The counter for the next field value
let mutable currentIdx = -1
/// Return the specified name for the parameter, or an anonymous parameter name if none is specified
member this.Derive paramName =
match paramName with
| Some it -> it
| None ->
currentIdx <- currentIdx + 1
$"@field{currentIdx}"
/// The required document serialization implementation
@@ -145,21 +213,10 @@ module Configuration =
[<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 field paramName =
let theRest = match field.Op with EX | NEX -> string field.Op | _ -> $"{field.Op} %s{paramName}"
$"data ->> '%s{field.Name}' {theRest}"
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Combine a query (select, update, etc.) and a WHERE clause
[<CompiledName "StatementWhere">]
let statementWhere statement where =
$"%s{statement} WHERE %s{where}"
/// Queries to define tables and indexes
module Definition =
@@ -176,7 +233,7 @@ module Query =
/// 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 ensureIndexOn tableName indexName (fields: string seq) dialect =
let _, tbl = splitSchemaAndTable tableName
let jsonFields =
fields
@@ -184,14 +241,14 @@ module Query =
let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"(data ->> '{fieldName}'){direction}")
$"({Field.NameToPath fieldName dialect}){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")
let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
/// Query to insert a document
[<CompiledName "Insert">]
@@ -205,59 +262,33 @@ module Query =
"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 =
/// Query to count documents in a table (no WHERE clause)
[<CompiledName "Count">]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Query to check for document existence in a table
[<CompiledName "Exists">]
let exists tableName where =
$"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it"
/// Queries for determining document existence
module Exists =
/// Query to select documents from a table (no WHERE clause)
[<CompiledName "Find">]
let find tableName =
$"SELECT data FROM %s{tableName}"
/// 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 update a document (no WHERE clause)
[<CompiledName "Update">]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Query to delete documents from a table (no WHERE clause)
[<CompiledName "Delete">]
let delete tableName =
$"DELETE FROM %s{tableName}"
/// 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 field =
$"""{selectFromTable tableName} WHERE {whereByField field "@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 field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Create a SELECT clause to retrieve the document data from the given table
[<CompiledName "SelectFromTable">]
[<System.Obsolete "Use Find instead">]
let selectFromTable tableName =
find tableName

View File

@@ -1,19 +1,19 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net6.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-2</VersionSuffix>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<FileVersion>3.1.0.0</FileVersion>
<VersionPrefix>3.1.0</VersionPrefix>
<PackageReleaseNotes>Add BT (between) operator; drop .NET 7 support</PackageReleaseNotes>
<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>
<RepositoryUrl>https://git.bitbadger.solutions/bit-badger/BitBadger.Documents</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Adds Field type for by-field operations (BREAKING from rc-1); adds RemoveFields* functions</PackageReleaseNotes>
<Description>Use PostgreSQL as a document database</Description>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
</PropertyGroup>
@@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,6 @@
namespace BitBadger.Documents.Postgres
open BitBadger.Documents
open Npgsql
open Npgsql.FSharp
@@ -50,8 +51,13 @@ module Extensions =
WithProps.Count.all tableName (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =)
member conn.countByFields tableName howMatched fields =
WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =)
[<System.Obsolete "Use countByFields instead; will be removed in v4">]
member conn.countByField tableName field =
WithProps.Count.byField tableName field (Sql.existingConnection conn)
conn.countByFields tableName Any [ field ]
/// Count matching documents using a JSON containment query (@>)
member conn.countByContains tableName criteria =
@@ -66,8 +72,13 @@ module Extensions =
WithProps.Exists.byId tableName docId (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
member conn.existsByFields tableName howMatched fields =
WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
[<System.Obsolete "Use existsByFields instead; will be removed in v4">]
member conn.existsByField tableName field =
WithProps.Exists.byField tableName field (Sql.existingConnection conn)
conn.existsByFields tableName Any [ field ]
/// Determine if documents exist using a JSON containment query (@>)
member conn.existsByContains tableName criteria =
@@ -86,8 +97,13 @@ module Extensions =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
member conn.findByFields<'TDoc> tableName howMatched fields =
WithProps.Find.byFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use findByFields instead; will be removed in v4">]
member conn.findByField<'TDoc> tableName field =
WithProps.Find.byField<'TDoc> tableName field (Sql.existingConnection conn)
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON containment query (@>)
member conn.findByContains<'TDoc> tableName (criteria: obj) =
@@ -98,8 +114,13 @@ module Extensions =
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.findFirstByFields<'TDoc> tableName howMatched fields =
WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<System.Obsolete "Use findFirstByFields instead; will be removed in v4">]
member conn.findFirstByField<'TDoc> tableName field =
WithProps.Find.firstByField<'TDoc> tableName field (Sql.existingConnection conn)
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
member conn.findFirstByContains<'TDoc> tableName (criteria: obj) =
@@ -122,8 +143,13 @@ module Extensions =
WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<System.Obsolete "Use patchByFields instead; will be removed in v4">]
member conn.patchByField tableName field (patch: 'TPatch) =
WithProps.Patch.byField tableName field patch (Sql.existingConnection conn)
conn.patchByFields tableName Any [ field ] patch
/// Patch documents using a JSON containment query in the WHERE clause (@>)
member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
@@ -137,9 +163,14 @@ module Extensions =
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on JSON fields in the document
member conn.removeFieldsByFields tableName howMatched fields fieldNames =
WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead; will be removed in v4">]
member conn.removeFieldsByField tableName field fieldNames =
WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn)
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// Remove fields from documents via a JSON containment query (@>)
member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames =
@@ -153,9 +184,13 @@ module Extensions =
member conn.deleteById tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (Sql.existingConnection conn)
member conn.deleteByFields tableName howMatched fields =
WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
[<System.Obsolete "Use deleteByFields instead; will be removed in v4">]
member conn.deleteByField tableName field =
WithProps.Delete.byField tableName field (Sql.existingConnection conn)
conn.deleteByFields tableName Any [ field ]
/// Delete documents by matching a JSON containment query (@>)
member conn.deleteByContains tableName (criteria: 'TContains) =
@@ -225,8 +260,14 @@ type NpgsqlConnectionCSharpExtensions =
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
static member inline CountByFields(conn, tableName, howMatched, fields) =
WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use CountByFields instead; will be removed in v4">]
static member inline CountByField(conn, tableName, field) =
WithProps.Count.byField tableName field (Sql.existingConnection conn)
conn.CountByFields(tableName, Any, [ field ])
/// Count matching documents using a JSON containment query (@>)
[<Extension>]
@@ -245,8 +286,14 @@ type NpgsqlConnectionCSharpExtensions =
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
static member inline ExistsByFields(conn, tableName, howMatched, fields) =
WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead; will be removed in v4">]
static member inline ExistsByField(conn, tableName, field) =
WithProps.Exists.byField tableName field (Sql.existingConnection conn)
conn.ExistsByFields(tableName, Any, [ field ])
/// Determine if documents exist using a JSON containment query (@>)
[<Extension>]
@@ -270,8 +317,14 @@ type NpgsqlConnectionCSharpExtensions =
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use FindByFields instead; will be removed in v4">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithProps.Find.ByField<'TDoc>(tableName, field, Sql.existingConnection conn)
conn.FindByFields<'TDoc>(tableName, Any, [ field ])
/// Retrieve documents matching a JSON containment query (@>)
[<Extension>]
@@ -283,10 +336,16 @@ type NpgsqlConnectionCSharpExtensions =
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
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead; will be removed in v4">]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) =
WithProps.Find.FirstByField<'TDoc>(tableName, field, Sql.existingConnection conn)
conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ])
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<Extension>]
@@ -315,8 +374,14 @@ type NpgsqlConnectionCSharpExtensions =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) =
WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
[<System.Obsolete "Use PatchByFields instead; will be removed in v4">]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) =
WithProps.Patch.byField tableName field patch (Sql.existingConnection conn)
conn.PatchByFields(tableName, Any, [ field ], patch)
/// Patch documents using a JSON containment query in the WHERE clause (@>)
[<Extension>]
@@ -331,22 +396,28 @@ type NpgsqlConnectionCSharpExtensions =
/// Remove fields from a document by the document's ID
[<Extension>]
static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) =
WithProps.RemoveFields.ById(tableName, docId, fieldNames, Sql.existingConnection conn)
WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on JSON fields in the document
[<Extension>]
static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead; will be removed in v4">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithProps.RemoveFields.ByField(tableName, field, fieldNames, Sql.existingConnection conn)
conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames)
/// Remove fields from documents via a JSON containment query (@>)
[<Extension>]
static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) =
WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, Sql.existingConnection conn)
WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn)
/// Remove fields from documents via a JSON Path match query (@?)
[<Extension>]
static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) =
WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, Sql.existingConnection conn)
WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (Sql.existingConnection conn)
/// Delete a document by its ID
[<Extension>]
@@ -355,8 +426,14 @@ type NpgsqlConnectionCSharpExtensions =
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
static member inline DeleteByFields(conn, tableName, howMatched, fields) =
WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead; will be removed in v4">]
static member inline DeleteByField(conn, tableName, field) =
WithProps.Delete.byField tableName field (Sql.existingConnection conn)
conn.DeleteByFields(tableName, Any, [ field ])
/// Delete documents by matching a JSON containment query (@>)
[<Extension>]

View File

@@ -46,6 +46,23 @@ module private Helpers =
()
}
/// Create a number or string parameter, or use the given parameter derivation function if non-(numeric or string)
let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) =
match box value with
| :? int8 as it -> Sql.int8 it
| :? uint8 as it -> Sql.int8 (int8 it)
| :? int16 as it -> Sql.int16 it
| :? uint16 as it -> Sql.int16 (int16 it)
| :? int as it -> Sql.int it
| :? uint32 as it -> Sql.int (int it)
| :? int64 as it -> Sql.int64 it
| :? uint64 as it -> Sql.int64 (int64 it)
| :? decimal as it -> Sql.decimal it
| :? single as it -> Sql.double (double it)
| :? double as it -> Sql.double it
| :? string as it -> Sql.string it
| _ -> catchAllFunc value
open BitBadger.Documents
@@ -53,40 +70,58 @@ open BitBadger.Documents
[<AutoOpen>]
module Parameters =
/// Create an ID parameter (name "@id", key will be treated as a string)
/// Create an ID parameter (name "@id")
[<CompiledName "Id">]
let idParam (key: 'TKey) =
"@id", Sql.string (string key)
"@id", parameterFor key (fun it -> Sql.string (string it))
/// 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 "FSharpAddField">]
/// Create JSON field parameters
[<CompiledName "AddFields">]
let addFieldParams fields parameters =
let name = ParameterName()
fields
|> Seq.map (fun it ->
seq {
match it.Op with
| EX | NEX -> ()
| BT ->
let p = name.Derive it.ParameterName
let values = it.Value :?> obj list
yield ($"{p}min",
parameterFor (List.head values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}min", v))))
yield ($"{p}max",
parameterFor (List.last values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}max", v))))
| _ ->
let p = name.Derive it.ParameterName
yield (p, parameterFor it.Value (fun v -> Sql.parameter (NpgsqlParameter(p, v)))) })
|> Seq.collect id
|> Seq.append parameters
|> Seq.toList
|> Seq.ofList
/// Create a JSON field parameter
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams (F#) / AddFields (C#) instead; will be removed in v4">]
let addFieldParam name field parameters =
match field.Op with
| EX | NEX -> parameters
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) :: parameters
/// Create a JSON field parameter (name "@field")
let AddField name field parameters =
match field.Op with
| EX | NEX -> parameters
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) |> Seq.singleton |> Seq.append parameters
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FSharpFieldName">]
let fieldNameParam (fieldNames: string list) =
if fieldNames.Length = 1 then "@name", Sql.string fieldNames[0]
else "@name", Sql.stringArray (Array.ofList fieldNames)
/// Append JSON field name parameters for the given field names to the given parameters
let FieldName(fieldNames: string seq) =
if Seq.isEmpty fieldNames then "@name", Sql.string (Seq.head fieldNames)
[<CompiledName "FieldNames">]
let fieldNameParams (fieldNames: string seq) =
if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames)
else "@name", Sql.stringArray (Array.ofSeq fieldNames)
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FieldName">]
[<System.Obsolete "Use fieldNameParams (F#) / FieldNames (C#) instead; will be removed in v4">]
let fieldNameParam fieldNames =
fieldNameParams fieldNames
/// An empty parameter sequence
[<CompiledName "None">]
let noParams =
@@ -97,6 +132,35 @@ module Parameters =
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "WhereByFields">]
let whereByFields (howMatched: FieldMatch) fields =
let name = ParameterName()
let isNumeric (it: obj) =
match it with
| :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64
| :? decimal | :? single | :? double -> true
| _ -> false
fields
|> Seq.map (fun it ->
match it.Op with
| EX | NEX -> $"{it.Path PostgreSQL} {it.Op}"
| _ ->
let p = name.Derive it.ParameterName
let param, value =
match it.Op with
| BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0]
| _ -> p, it.Value
if isNumeric value then
$"({it.Path PostgreSQL})::numeric {it.Op} {param}"
else $"{it.Path PostgreSQL} {it.Op} {param}")
|> String.concat $" {howMatched} "
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById<'TKey> (docId: 'TKey) =
whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ]
/// Table and index definition queries
module Definition =
@@ -122,111 +186,35 @@ module Query =
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 =
/// Create an UPDATE statement to patch documents
let private update tableName whereClause =
$"UPDATE %s{tableName} SET data = data || @data WHERE {whereClause}"
[<CompiledName "Patch">]
let patch tableName =
$"UPDATE %s{tableName} SET data = data || @data"
/// Query to patch a document by its ID
/// Create an UPDATE statement to remove fields from documents
[<CompiledName "RemoveFields">]
let removeFields tableName =
$"UPDATE %s{tableName} SET data = data - @name"
/// Create a query by a document's ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
let byId<'TKey> statement (docId: 'TKey) =
Query.statementWhere statement (whereById docId)
/// Query to patch documents match a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName field =
Query.whereByField field "@field" |> update tableName
/// Create a query on JSON fields
[<CompiledName "ByFields">]
let byFields statement howMatched fields =
Query.statementWhere statement (whereByFields howMatched fields)
/// Query to patch documents matching a JSON containment query (@>)
/// Create a JSON containment query
[<CompiledName "ByContains">]
let byContains tableName =
whereDataContains "@criteria" |> update tableName
let byContains statement =
Query.statementWhere statement (whereDataContains "@criteria")
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
whereJsonPathMatches "@path" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
/// Create an UPDATE statement to remove parameters
let private update tableName whereClause =
$"UPDATE %s{tableName} SET data = data - @name WHERE {whereClause}"
/// Query to remove fields from a document by the document's ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
/// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "ByField">]
let byField tableName field =
Query.whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
whereDataContains "@criteria" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByJsonPath">]
let byJsonPath tableName =
whereJsonPathMatches "@path" |> update tableName
/// 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"}"""
/// Create a JSON Path match query
[<CompiledName "ByPathMatch">]
let byPathMatch statement =
Query.statementWhere statement (whereJsonPathMatches "@path")
/// Functions for dealing with results
@@ -257,6 +245,8 @@ module Results =
/// Versions of queries that accept SqlProps as the last parameter
module WithProps =
module FSharpList = Microsoft.FSharp.Collections.List
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
@@ -265,12 +255,12 @@ module WithProps =
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps =
Sql.query query sqlProps
|> Sql.parameters parameters
|> Sql.parameters (List.ofSeq 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
let! results = list<'TDoc> query parameters mapFunc.Invoke sqlProps
return ResizeArray results
}
@@ -284,7 +274,7 @@ module WithProps =
/// 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
let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps
return Option.toObj result
}
@@ -292,7 +282,7 @@ module WithProps =
[<CompiledName "NonQuery">]
let nonQuery query parameters sqlProps =
Sql.query query sqlProps
|> Sql.parameters (FSharp.Collections.List.ofSeq parameters)
|> Sql.parameters (FSharpList.ofSeq parameters)
|> Sql.executeNonQueryAsync
|> ignoreTask
@@ -300,12 +290,12 @@ module WithProps =
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps =
Sql.query query sqlProps
|> Sql.parameters parameters
|> Sql.parameters (FSharpList.ofSeq 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
scalar<'T> query parameters mapFunc.Invoke sqlProps
/// Table and index definition commands
module Definition =
@@ -314,7 +304,7 @@ module WithProps =
[<CompiledName "EnsureTable">]
let ensureTable name sqlProps = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps
do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps
do! Custom.nonQuery (Query.Definition.ensureKey name PostgreSQL) [] sqlProps
}
/// Create an index on documents in the specified table
@@ -325,7 +315,7 @@ module WithProps =
/// 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
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields PostgreSQL) [] sqlProps
/// Commands to add documents
[<AutoOpen>]
@@ -348,22 +338,25 @@ module WithProps =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName sqlProps =
Custom.scalar (Query.Count.all tableName) [] toCount sqlProps
Custom.scalar (Query.count tableName) [] toCount sqlProps
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName field sqlProps =
Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount sqlProps
/// Count matching documents using JSON field comparisons (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.scalar
(Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) 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
Custom.scalar
(Query.byContains (Query.count 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
Custom.scalar
(Query.byPathMatch (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
@@ -372,22 +365,34 @@ module WithProps =
/// 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
Custom.scalar (Query.exists tableName (Query.whereById docId)) [ idParam docId ] toExists sqlProps
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName field sqlProps =
Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists sqlProps
/// Determine if a document exists using JSON field comparisons (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.scalar
(Query.exists tableName (Query.whereByFields howMatched fields))
(addFieldParams fields [])
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
Custom.scalar
(Query.exists tableName (Query.whereDataContains "@criteria"))
[ 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
Custom.scalar
(Query.exists tableName (Query.whereJsonPathMatches "@path"))
[ "@path", Sql.string jsonPath ]
toExists
sqlProps
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
@@ -396,81 +401,119 @@ module WithProps =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName sqlProps =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps
Custom.list<'TDoc> (Query.find 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)
Custom.List<'TDoc>(Query.find 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
Custom.single (Query.byId (Query.find tableName) docId) [ 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)
Custom.Single<'TDoc>(
Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps)
/// Retrieve documents matching JSON field comparisons (->> =)
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields sqlProps =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
fromData<'TDoc>
sqlProps
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField<'TDoc> tableName field sqlProps =
Custom.list<'TDoc>
(Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> sqlProps
byFields<'TDoc> tableName Any [ field ] sqlProps
/// Retrieve documents matching JSON field comparisons (->> =)
let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched fields,
addFieldParams fields [],
fromData<'TDoc>,
sqlProps)
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let ByField<'TDoc>(tableName, field, sqlProps) =
Custom.List<'TDoc>(
Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, sqlProps)
ByFields<'TDoc>(tableName, Any, Seq.singleton field, 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
(Query.byContains (Query.find 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)
Query.byContains (Query.find 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
(Query.byPathMatch (Query.find 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)
Query.byPathMatch (Query.find 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 field sqlProps =
/// Retrieve the first document matching JSON field comparisons (->> =); returns None if not found
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields sqlProps =
Custom.single<'TDoc>
$"{Query.Find.byField tableName field} LIMIT 1"
(addFieldParam "@field" field [])
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1"
(addFieldParams fields [])
fromData<'TDoc>
sqlProps
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) =
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead; will be removed in v4">]
let firstByField<'TDoc> tableName field sqlProps =
firstByFields<'TDoc> tableName Any [ field ] sqlProps
/// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) =
Custom.Single<'TDoc>(
$"{Query.Find.byField tableName field} LIMIT 1",
addFieldParam "@field" field [],
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [],
fromData<'TDoc>,
sqlProps)
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead; will be removed in v4">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) =
FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, 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
$"{Query.byContains (Query.find 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",
$"{Query.byContains (Query.find tableName)} LIMIT 1",
[ jsonParam "@criteria" criteria ],
fromData<'TDoc>,
sqlProps)
@@ -479,12 +522,15 @@ module WithProps =
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath sqlProps =
Custom.single<'TDoc>
$"{Query.Find.byJsonPath tableName} LIMIT 1" [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps
$"{Query.byPathMatch (Query.find 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",
$"{Query.byPathMatch (Query.find tableName)} LIMIT 1",
[ "@path", Sql.string jsonPath ],
fromData<'TDoc>,
sqlProps)
@@ -496,7 +542,8 @@ module WithProps =
/// 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
Custom.nonQuery
(Query.byId (Query.update tableName) docId) [ 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">]
@@ -514,77 +561,79 @@ module WithProps =
/// 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
Custom.nonQuery
(Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] sqlProps
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.byFields (Query.patch tableName) howMatched fields)
(addFieldParams fields [ jsonParam "@data" patch ])
sqlProps
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field (patch: 'TPatch) sqlProps =
Custom.nonQuery
(Query.Patch.byField tableName field)
(addFieldParam "@field" field [ jsonParam "@data" patch ])
sqlProps
byFields tableName Any [ field ] patch 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
(Query.byContains (Query.patch 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
(Query.byPathMatch (Query.patch tableName))
[ jsonParam "@data" patch; "@path", Sql.string jsonPath ]
sqlProps
/// Commands to remove fields from documents
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames sqlProps =
Custom.nonQuery (Query.RemoveFields.byId tableName) [ idParam docId; fieldNameParam fieldNames ] sqlProps
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames, sqlProps) =
byId tableName docId (List.ofSeq fieldNames) sqlProps
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames sqlProps =
Custom.nonQuery
(Query.RemoveFields.byField tableName field)
(addFieldParam "@field" field [ fieldNameParam fieldNames ])
(Query.byId (Query.removeFields tableName) docId) [ idParam docId; fieldNameParams fieldNames ] sqlProps
/// Remove fields from documents via a comparison on JSON fields in the document
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames sqlProps =
Custom.nonQuery
(Query.byFields (Query.removeFields tableName) howMatched fields)
(addFieldParams fields [ fieldNameParams fieldNames ])
sqlProps
/// Remove fields from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames, sqlProps) =
byField tableName field (List.ofSeq fieldNames) sqlProps
[<CompiledName "ByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField tableName field fieldNames sqlProps =
byFields tableName Any [ field ] fieldNames sqlProps
/// Remove fields from documents via a JSON containment query (@>)
[<CompiledName "FSharpByContains">]
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) fieldNames sqlProps =
Custom.nonQuery
(Query.RemoveFields.byContains tableName)
[ jsonParam "@criteria" criteria; fieldNameParam fieldNames ]
(Query.byContains (Query.removeFields tableName))
[ jsonParam "@criteria" criteria; fieldNameParams fieldNames ]
sqlProps
/// Remove fields from documents via a JSON containment query (@>)
let ByContains(tableName, criteria: 'TContains, fieldNames, sqlProps) =
byContains tableName criteria (List.ofSeq fieldNames) sqlProps
/// Remove fields from documents via a JSON Path match query (@?)
[<CompiledName "FSharpByJsonPath">]
let byJsonPath tableName jsonPath fieldNames sqlProps =
Custom.nonQuery
(Query.RemoveFields.byJsonPath tableName)
[ "@path", Sql.string jsonPath; fieldNameParam fieldNames ]
(Query.byPathMatch (Query.removeFields tableName))
[ "@path", Sql.string jsonPath; fieldNameParams fieldNames ]
sqlProps
/// Remove fields from documents via a JSON Path match query (@?)
let ByJsonPath(tableName, jsonPath, fieldNames, sqlProps) =
byJsonPath tableName jsonPath (List.ofSeq fieldNames) sqlProps
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
@@ -592,22 +641,29 @@ module WithProps =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) sqlProps =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps
Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] sqlProps
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields sqlProps =
Custom.nonQuery
(Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) sqlProps
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field sqlProps =
Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) sqlProps
byFields tableName Any [ field ] 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
Custom.nonQuery (Query.byContains (Query.delete 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
Custom.nonQuery (Query.byPathMatch (Query.delete tableName)) [ "@path", Sql.string path ] sqlProps
/// Commands to execute custom SQL queries
@@ -691,10 +747,16 @@ module Count =
let all tableName =
WithProps.Count.all tableName (fromDataSource ())
/// Count matching documents using a JSON field comparison query (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Count.byFields tableName howMatched fields (fromDataSource ())
/// Count matching documents using a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
WithProps.Count.byField tableName field (fromDataSource ())
byFields tableName Any [ field ]
/// Count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
@@ -716,10 +778,16 @@ module Exists =
let byId tableName docId =
WithProps.Exists.byId tableName docId (fromDataSource ())
/// Determine if documents exist using a JSON field comparison query (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Exists.byFields tableName howMatched fields (fromDataSource ())
/// Determine if documents exist using a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
WithProps.Exists.byField tableName field (fromDataSource ())
byFields tableName Any [ field ]
/// Determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">]
@@ -755,13 +823,24 @@ module Find =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =)
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName field =
WithProps.Find.byField<'TDoc> tableName field (fromDataSource ())
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields =
WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField<'TDoc> tableName field =
byFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison query (->> =)
let ByFields<'TDoc>(tableName, howMatched, fields) =
WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let ByField<'TDoc>(tableName, field) =
WithProps.Find.ByField<'TDoc>(tableName, field, fromDataSource ())
ByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve documents matching a JSON containment query (@>)
[<CompiledName "FSharpByContains">]
@@ -781,14 +860,25 @@ module Find =
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 "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead; will be removed in v4">]
let firstByField<'TDoc> tableName field =
WithProps.Find.firstByField<'TDoc> tableName field (fromDataSource ())
firstByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead; will be removed in v4">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) =
WithProps.Find.FirstByField<'TDoc>(tableName, field, fromDataSource ())
FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<CompiledName "FSharpFirstByContains">]
@@ -837,10 +927,16 @@ module Patch =
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 "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) =
WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ())
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field (patch: 'TPatch) =
WithProps.Patch.byField tableName field patch (fromDataSource ())
byFields tableName Any [ field ] patch
/// Patch documents using a JSON containment query in the WHERE clause (@>)
[<CompiledName "ByContains">]
@@ -858,41 +954,31 @@ module Patch =
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames =
WithProps.RemoveFields.byId tableName docId fieldNames (fromDataSource ())
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames) =
WithProps.RemoveFields.ById(tableName, docId, fieldNames, fromDataSource ())
/// Remove fields from documents via a comparison on JSON fields in the document
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ())
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
[<CompiledName "ByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField tableName field fieldNames =
WithProps.RemoveFields.byField tableName field fieldNames (fromDataSource ())
/// Remove fields from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames) =
WithProps.RemoveFields.ByField(tableName, field, fieldNames, fromDataSource ())
byFields tableName Any [ field ] fieldNames
/// Remove fields from documents via a JSON containment query (@>)
[<CompiledName "FSharpByContains">]
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) fieldNames =
WithProps.RemoveFields.byContains tableName criteria fieldNames (fromDataSource ())
/// Remove fields from documents via a JSON containment query (@>)
let ByContains(tableName, criteria: 'TContains, fieldNames) =
WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, fromDataSource ())
/// Remove fields from documents via a JSON Path match query (@?)
[<CompiledName "FSharpByJsonPath">]
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath fieldNames =
WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (fromDataSource ())
/// Remove fields from documents via a JSON Path match query (@?)
let ByJsonPath(tableName, jsonPath, fieldNames) =
WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, fromDataSource ())
/// Commands to delete documents
[<RequireQualifiedAccess>]
@@ -903,10 +989,16 @@ module Delete =
let byId tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (fromDataSource ())
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Delete.byFields tableName howMatched fields (fromDataSource ())
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
WithProps.Delete.byField tableName field (fromDataSource ())
byFields tableName Any [ field ]
/// Delete documents by matching a JSON containment query (@>)
[<CompiledName "ByContains">]

View File

@@ -45,7 +45,7 @@ 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");
var customers = await Find.All<Customer>("customer");
```
```fsharp

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageReleaseNotes>Adds Field type for by-field operations (BREAKING from rc-1); adds RemoveFields* functions</PackageReleaseNotes>
<Description>Use SQLite as a document database</Description>
<PackageTags>JSON Document SQLite</PackageTags>
</PropertyGroup>
@@ -13,7 +13,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,6 @@
namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite
/// F# extensions for the SqliteConnection type
@@ -44,17 +45,27 @@ module Extensions =
member conn.countAll tableName =
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on JSON fields
member conn.countByFields tableName howMatched fields =
WithConn.Count.byFields tableName howMatched fields conn
/// Count matching documents using a comparison on a JSON field
[<System.Obsolete "Use countByFields instead; will be removed in v4">]
member conn.countByField tableName field =
WithConn.Count.byField tableName field conn
conn.countByFields tableName Any [ field ]
/// 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 JSON fields
member conn.existsByFields tableName howMatched fields =
WithConn.Exists.byFields tableName howMatched fields conn
/// Determine if a document exists using a comparison on a JSON field
[<System.Obsolete "Use existsByFields instead; will be removed in v4">]
member conn.existsByField tableName field =
WithConn.Exists.byField tableName field conn
conn.existsByFields tableName Any [ field ]
/// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName =
@@ -64,13 +75,23 @@ module Extensions =
member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) =
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve documents via a comparison on JSON fields
member conn.findByFields<'TDoc> tableName howMatched fields =
WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field
[<System.Obsolete "Use findByFields instead; will be removed in v4">]
member conn.findByField<'TDoc> tableName field =
WithConn.Find.byField<'TDoc> tableName field conn
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve documents via a comparison on JSON fields, returning only the first result
member conn.findFirstByFields<'TDoc> tableName howMatched fields =
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<System.Obsolete "Use findFirstByFields instead; will be removed in v4">]
member conn.findFirstByField<'TDoc> tableName field =
WithConn.Find.firstByField<'TDoc> tableName field conn
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
@@ -84,25 +105,40 @@ module Extensions =
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on JSON fields
member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
WithConn.Patch.byFields tableName howMatched fields patch conn
/// Patch documents using a comparison on a JSON field
[<System.Obsolete "Use patchByFields instead; will be removed in v4">]
member conn.patchByField tableName field (patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn
conn.patchByFields tableName Any [ field ] patch
/// Remove fields from a document by the document's ID
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove a field from a document via a comparison on JSON fields in the document
member conn.removeFieldsByFields tableName howMatched fields fieldNames =
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Remove a field from a document via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead; will be removed in v4">]
member conn.removeFieldsByField tableName field fieldNames =
WithConn.RemoveFields.byField tableName field fieldNames conn
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// 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 JSON fields
member conn.deleteByFields tableName howMatched fields =
WithConn.Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
[<System.Obsolete "Use deleteByFields instead; will be removed in v4">]
member conn.deleteByField tableName field =
WithConn.Delete.byField tableName field conn
conn.deleteByFields tableName Any [ field ]
open System.Runtime.CompilerServices
@@ -157,20 +193,32 @@ type SqliteConnectionCSharpExtensions =
static member inline CountAll(conn, tableName) =
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on JSON fields
[<Extension>]
static member inline CountByFields(conn, tableName, howMatched, fields) =
WithConn.Count.byFields tableName howMatched fields conn
/// Count matching documents using a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use CountByFields instead; will be removed in v4">]
static member inline CountByField(conn, tableName, field) =
WithConn.Count.byField tableName field conn
conn.CountByFields(tableName, Any, [ field ])
/// 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 JSON fields
[<Extension>]
static member inline ExistsByFields(conn, tableName, howMatched, fields) =
WithConn.Exists.byFields tableName howMatched fields conn
/// Determine if a document exists using a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead; will be removed in v4">]
static member inline ExistsByField(conn, tableName, field) =
WithConn.Exists.byField tableName field conn
conn.ExistsByFields(tableName, Any, [ field ])
/// Retrieve all documents in the given table
[<Extension>]
@@ -182,15 +230,27 @@ type SqliteConnectionCSharpExtensions =
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 JSON fields
[<Extension>]
static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use FindByFields instead; will be removed in v4">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithConn.Find.ByField<'TDoc>(tableName, field, conn)
conn.FindByFields<'TDoc>(tableName, Any, [ field ])
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) =
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead; will be removed in v4">]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) =
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn)
conn.FindFirstByFields<'TDoc>(tableName, Any, [ field ])
/// Update an entire document by its ID
[<Extension>]
@@ -207,27 +267,45 @@ type SqliteConnectionCSharpExtensions =
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 JSON fields
[<Extension>]
static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) =
WithConn.Patch.byFields tableName howMatched fields patch conn
/// Patch documents using a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use PatchByFields instead; will be removed in v4">]
static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn
conn.PatchByFields(tableName, Any, [ field ], patch)
/// Remove fields from a document by the document's ID
[<Extension>]
static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) =
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn)
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove fields from documents via a comparison on JSON fields in the document
[<Extension>]
static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead; will be removed in v4">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn)
conn.RemoveFieldsByFields(tableName, Any, [ field ], fieldNames)
/// 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 JSON fields
[<Extension>]
static member inline DeleteByFields(conn, tableName, howMatched, fields) =
WithConn.Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead; will be removed in v4">]
static member inline DeleteByField(conn, tableName, field) =
WithConn.Delete.byField tableName field conn
conn.DeleteByFields(tableName, Any, [ field ])

View File

@@ -31,6 +31,48 @@ module Configuration =
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on fields in a JSON document
[<CompiledName "WhereByFields">]
let whereByFields (howMatched: FieldMatch) fields =
let name = ParameterName()
fields
|> Seq.map (fun it ->
match it.Op with
| EX | NEX -> $"{it.Path SQLite} {it.Op}"
| BT ->
let p = name.Derive it.ParameterName
$"{it.Path SQLite} {it.Op} {p}min AND {p}max"
| _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}")
|> String.concat $" {howMatched} "
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ]
/// Create an UPDATE statement to patch documents
[<CompiledName "Patch">]
let patch tableName =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data))"
/// Create an UPDATE statement to remove fields from documents
[<CompiledName "RemoveFields">]
let removeFields tableName (parameters: SqliteParameter seq) =
let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames})"
/// Create a query by a document's ID
[<CompiledName "ById">]
let byId<'TKey> statement (docId: 'TKey) =
Query.statementWhere
statement
(whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ])
/// Create a query on JSON fields
[<CompiledName "ByFields">]
let byFields statement howMatched fields =
Query.statementWhere statement (whereByFields howMatched fields)
/// Data definition
module Definition =
@@ -39,49 +81,6 @@ module Query =
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
/// Document patching (partial update) queries
module Patch =
/// Create an UPDATE statement to patch documents
let internal update tableName whereClause =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE %s{whereClause}"
/// Query to patch (partially update) a document by its ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
/// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
Query.whereByField field "@field" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
/// Create an UPDATE statement to remove parameters
let internal update tableName (parameters: SqliteParameter list) whereClause =
let paramNames = parameters |> List.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE {whereClause}"
/// Query to remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName parameters =
Query.whereById "@id" |> update tableName parameters
/// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) =
byId tableName (List.ofSeq parameters)
/// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "FSharpByField">]
let byField tableName field parameters =
Query.whereByField field "@field" |> update tableName parameters
/// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters)
/// Parameter handling helpers
[<AutoOpen>]
@@ -97,25 +96,39 @@ module Parameters =
let jsonParam name (it: 'TJson) =
SqliteParameter(name, Configuration.serializer().Serialize it)
/// Create JSON field parameters
[<CompiledName "AddFields">]
let addFieldParams fields parameters =
let name = ParameterName()
fields
|> Seq.map (fun it ->
seq {
match it.Op with
| EX | NEX -> ()
| BT ->
let p = name.Derive it.ParameterName
let values = it.Value :?> obj list
yield SqliteParameter($"{p}min", List.head values)
yield SqliteParameter($"{p}max", List.last values)
| _ -> yield SqliteParameter(name.Derive it.ParameterName, it.Value) })
|> Seq.collect id
|> Seq.append parameters
|> Seq.toList
|> Seq.ofList
/// Create a JSON field parameter (name "@field")
[<CompiledName "FSharpAddField">]
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams instead; will be removed in v4">]
let addFieldParam name field parameters =
match field.Op with EX | NEX -> parameters | _ -> SqliteParameter(name, field.Value) :: parameters
/// Create a JSON field parameter (name "@field")
let AddField(name, field, parameters) =
match field.Op with
| EX | NEX -> parameters
| _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FSharpFieldNames">]
let fieldNameParams paramName (fieldNames: string list) =
fieldNames |> List.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}"))
/// Append JSON field name parameters for the given field names to the given parameters
let FieldNames(paramName, fieldNames: string seq) =
fieldNames |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}"))
[<CompiledName "FieldNames">]
let fieldNameParams paramName fieldNames =
fieldNames
|> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.%s{name}"))
|> Seq.toList
|> Seq.ofList
/// An empty parameter sequence
[<CompiledName "None">]
@@ -237,13 +250,13 @@ module WithConn =
[<CompiledName "EnsureTable">]
let ensureTable name conn = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name SQLite) [] conn
}
/// Create an index on a document table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn
/// Insert a new document
[<CompiledName "Insert">]
@@ -262,12 +275,13 @@ module WithConn =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName conn =
Custom.scalar (Query.Count.all tableName) [] toCount conn
Custom.scalar (Query.count tableName) [] toCount conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field conn =
Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount conn
/// Count matching documents using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.scalar
(Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
@@ -276,12 +290,16 @@ module WithConn =
/// 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
Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field conn =
Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists conn
/// Determine if a document exists using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.scalar
(Query.exists tableName (Query.whereByFields howMatched fields))
(addFieldParams fields [])
toExists
conn
/// Commands to retrieve documents
[<RequireQualifiedAccess>]
@@ -290,42 +308,76 @@ module WithConn =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName conn =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn
Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> conn
/// Retrieve all documents in the given table
let All<'TDoc>(tableName, conn) =
Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn)
Custom.List(Query.find 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
Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) =
Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn)
Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on JSON fields
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields conn =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on a JSON field
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField<'TDoc> tableName field conn =
Custom.list<'TDoc>
(Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> conn
byFields<'TDoc> tableName Any [ field ] conn
/// Retrieve documents via a comparison on JSON fields
let ByFields<'TDoc>(tableName, howMatched, fields, conn) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched fields,
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// Retrieve documents via a comparison on a JSON field
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let ByField<'TDoc>(tableName, field, conn) =
Custom.List<'TDoc>(
Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, conn)
ByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields conn =
Custom.single
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1"
(addFieldParams fields [])
fromData<'TDoc>
conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead; will be removed in v4">]
let firstByField<'TDoc> tableName field conn =
Custom.single
$"{Query.Find.byField tableName field} LIMIT 1" (addFieldParam "@field" field []) fromData<'TDoc> conn
firstByFields<'TDoc> tableName Any [ field ] conn
/// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) =
Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<System.Obsolete "Use FirstByFields instead; will be removed in v4">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) =
Custom.Single(
$"{Query.Find.byField tableName field} LIMIT 1", addFieldParam "@field" field [], fromData<'TDoc>, conn)
FirstByFields(tableName, Any, [ field ], conn)
/// Commands to update documents
[<RequireQualifiedAccess>]
@@ -334,7 +386,10 @@ module WithConn =
/// 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
Custom.nonQuery
(Query.statementWhere (Query.update tableName) (Query.whereById "@id"))
[ idParam docId; jsonParam "@data" document ]
conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
@@ -352,38 +407,50 @@ module WithConn =
/// 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
Custom.nonQuery
(Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] conn
/// Patch documents using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) conn =
Custom.nonQuery
(Query.byFields (Query.patch tableName) howMatched fields)
(addFieldParams fields [ jsonParam "@data" patch ])
conn
/// Patch documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) =
Custom.nonQuery
(Query.Patch.byField tableName field) (addFieldParam "@field" field [ jsonParam "@data" patch ]) conn
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field (patch: 'TPatch) conn =
byFields tableName Any [ field ] patch conn
/// Commands to remove fields from documents
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery (Query.RemoveFields.byId tableName nameParams) (idParam docId :: nameParams) conn
Custom.nonQuery
(Query.byId (Query.removeFields tableName nameParams) docId)
(idParam docId |> Seq.singleton |> Seq.append nameParams)
conn
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames, conn) =
byId tableName docId (List.ofSeq fieldNames) conn
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames conn =
/// Remove fields from documents via a comparison on JSON fields in the document
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery
(Query.RemoveFields.byField tableName field nameParams) (addFieldParam "@field" field nameParams) conn
(Query.byFields (Query.removeFields tableName nameParams) howMatched fields)
(addFieldParams fields nameParams)
conn
/// Remove fields from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames, conn) =
byField tableName field (List.ofSeq fieldNames) conn
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field fieldNames conn =
byFields tableName Any [ field ] fieldNames conn
/// Commands to delete documents
[<RequireQualifiedAccess>]
@@ -392,12 +459,18 @@ module WithConn =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn
Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] conn
/// Delete documents by matching a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field conn =
Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) conn
byFields tableName Any [ field ] conn
/// Commands to execute custom SQL queries
@@ -485,11 +558,17 @@ module Count =
use conn = Configuration.dbConn ()
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Count.byFields tableName howMatched fields conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Count.byField tableName field conn
byFields tableName Any [ field ]
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
@@ -501,11 +580,17 @@ module Exists =
use conn = Configuration.dbConn ()
WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Exists.byFields tableName howMatched fields conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Exists.byField tableName field conn
byFields tableName Any [ field ]
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
@@ -533,27 +618,49 @@ module Find =
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 field =
/// Retrieve documents via a comparison on JSON fields
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Find.byField<'TDoc> tableName field conn
WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field
let ByField<'TDoc>(tableName, field) =
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead; will be removed in v4">]
let byField<'TDoc> tableName field =
byFields tableName Any [ field ]
/// Retrieve documents via a comparison on JSON fields
let ByFields<'TDoc>(tableName, howMatched, fields) =
use conn = Configuration.dbConn ()
WithConn.Find.ByField<'TDoc>(tableName, field, conn)
WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let ByField<'TDoc>(tableName, field) =
ByFields<'TDoc>(tableName, Any, [ field ])
/// Retrieve documents via a comparison on JSON fields, returning only the first result
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead; will be removed in v4">]
let firstByField<'TDoc> tableName field =
firstByFields<'TDoc> tableName Any [ field ]
/// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) =
use conn = Configuration.dbConn ()
WithConn.Find.firstByField<'TDoc> tableName field conn
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<System.Obsolete "Use FirstByFields instead; will be removed in v4">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn)
FirstByFields<'TDoc>(tableName, Any, [ field ])
/// Commands to update documents
[<RequireQualifiedAccess>]
@@ -586,37 +693,39 @@ module Patch =
use conn = Configuration.dbConn ()
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on JSON fields in the WHERE clause
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byFields tableName howMatched fields patch conn
/// Patch documents using a comparison on a JSON field in the WHERE clause
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byField tableName field patch conn
byFields tableName Any [ field ] patch
/// Commands to remove fields from documents
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames) =
/// Remove field from documents via a comparison on JSON fields in the document
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn)
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Remove field from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byField tableName field fieldNames conn
/// Remove field from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames) =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn)
byFields tableName Any [ field ] fieldNames
/// Commands to delete documents
[<RequireQualifiedAccess>]
@@ -628,8 +737,14 @@ module Delete =
use conn = Configuration.dbConn ()
WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on JSON fields
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead; will be removed in v4">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Delete.byField tableName field conn
byFields tableName Any [ field ]

View File

@@ -45,7 +45,7 @@ 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");
var customers = await Find.All<Customer>("customer");
```
```fsharp
@@ -72,28 +72,28 @@ 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");
// Count.ByField type signature is Func<string, Field, Task<long>>
var customerCount = await Count.ByField("customer", Field.EQ("City", "Atlanta"));
```
```fsharp
// F#
// Count.byField type signature is string -> string -> Op -> obj -> Task<int64>
let! customerCount = Count.byField "customer" "City" EQ "Atlanta"
// Count.byField type signature is string -> Field -> Task<int64>
let! customerCount = Count.byField "customer" (Field.EQ "City" "Atlanta")
```
Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp
// C#; parameters are same as above, except return is void
// Delete.ByField type signature is Func<string, string, Op, object, Task>
await Delete.ByField("customer", "City", Op.EQ, "Chicago");
// Delete.ByField type signature is Func<string, Field, Task>
await Delete.ByField("customer", Field.EQ("City", "Chicago"));
```
```fsharp
// F#
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit>
do! Delete.byField "customer" "City" EQ "Chicago"
do! Delete.byField "customer" (Field.EQ "City" "Chicago")
```
## More Information

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
@@ -12,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" />
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" />
</ItemGroup>

View File

@@ -1,5 +1,7 @@
using Expecto.CSharp;
using Expecto;
using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Core;
namespace BitBadger.Documents.Tests.CSharp;
@@ -23,11 +25,11 @@ public static class CommonCSharpTests
/// Unit tests
/// </summary>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit", new[]
{
public static readonly Test Unit = TestList("Common.C# Unit",
[
TestSequenced(
TestList("Configuration", new[]
{
TestList("Configuration",
[
TestCase("UseSerializer succeeds", () =>
{
try
@@ -69,9 +71,9 @@ public static class CommonCSharpTests
Configuration.UseIdField("Id");
}
})
})),
TestList("Op", new[]
{
])),
TestList("Op",
[
TestCase("EQ succeeds", () =>
{
Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct");
@@ -96,6 +98,10 @@ public static class CommonCSharpTests
{
Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct");
}),
TestCase("BT succeeds", () =>
{
Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct");
}),
TestCase("EX succeeds", () =>
{
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct");
@@ -104,9 +110,9 @@ public static class CommonCSharpTests
{
Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct");
})
}),
TestList("Field", new[]
{
]),
TestList("Field",
[
TestCase("EQ succeeds", () =>
{
var field = Field.EQ("Test", 14);
@@ -149,6 +155,13 @@ public static class CommonCSharpTests
Expect.equal(field.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value incorrect");
}),
TestCase("BT succeeds", () =>
{
var field = Field.BT("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Op, Op.BT, "Operator incorrect");
Expect.equal(((FSharpList<object>)field.Value).ToArray(), [18, 49], "Value incorrect");
}),
TestCase("EX succeeds", () =>
{
var field = Field.EX("Groovy");
@@ -160,65 +173,159 @@ public static class CommonCSharpTests
var field = Field.NEX("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.equal(field.Op, Op.NEX, "Operator incorrect");
}),
TestCase("WithParameterName succeeds", () =>
{
var field = Field.EQ("Bob", "Tom").WithParameterName("@name");
Expect.isSome(field.ParameterName, "The parameter name should have been filled");
Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect");
}),
TestCase("WithQualifier succeeds", () =>
{
var field = Field.EQ("Bill", "Matt").WithQualifier("joe");
Expect.isSome(field.Qualifier, "The table qualifier should have been filled");
Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect");
}),
TestList("Path",
[
TestCase("succeeds for a PostgreSQL single field with no qualifier", () =>
{
var field = Field.GE("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL single field with a qualifier", () =>
{
var field = Field.LT("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with no qualifier", () =>
{
var field = Field.EQ("My.Nested.Field", "howdy");
Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with a qualifier", () =>
{
var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a SQLite single field with no qualifier", () =>
{
var field = Field.GE("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite single field with a qualifier", () =>
{
var field = Field.LT("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with no qualifier", () =>
{
var field = Field.EQ("My.Nested.Field", "howdy");
Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with a qualifier", () =>
{
var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite),
"The SQLite path is incorrect");
})
])
]),
TestList("FieldMatch.ToString",
[
TestCase("succeeds for Any", () =>
{
Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect");
}),
TestList("Query", new[]
TestCase("succeeds for All", () =>
{
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(Field.GT("theField", 0), "@test"), "data ->> 'theField' > @test",
"WHERE clause not correct");
}),
TestCase("succeeds when an existence operator is passed", () =>
{
Expect.equal(Query.WhereByField(Field.NEX("thatField"), ""), "data ->> 'thatField' IS NULL",
"WHERE clause not correct");
Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect");
})
}),
TestList("Definition", new[]
]),
TestList("ParameterName.Derive",
[
TestCase("succeeds with existing name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.Some("@taco")), "@taco", "Name should have been @taco");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Counter should not have advanced for named field");
}),
TestCase("Derive succeeds with non-existent name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Anonymous field name should have been returned");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field1",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field2",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field3",
"Counter should have advanced from previous call");
})
]),
TestList("Query",
[
TestCase("StatementWhere succeeds", () =>
{
Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly");
}),
TestList("Definition",
[
TestCase("EnsureTableFor succeeds", () =>
{
Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"),
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
"CREATE TABLE statement not constructed correctly");
}),
TestList("EnsureKey", new[]
{
TestList("EnsureKey",
[
TestCase("succeeds when a schema is present", () =>
{
Expect.equal(Query.Definition.EnsureKey("test.table"),
Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))",
"CREATE INDEX for key statement with schema not constructed correctly");
}),
TestCase("succeeds when a schema is not present", () =>
{
Expect.equal(Query.Definition.EnsureKey("table"),
Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))",
"CREATE INDEX for key statement without schema not constructed correctly");
})
}),
TestCase("EnsureIndexOn succeeds for multiple fields and directions", () =>
]),
TestList("EnsureIndexOn",
[
TestCase("succeeds for multiple fields and directions", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("test.table", "gibberish",
new[] { "taco", "guac DESC", "salsa ASC" }),
new[] { "taco", "guac DESC", "salsa ASC" }, Dialect.SQLite),
"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("succeeds for nested PostgreSQL field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))",
"CREATE INDEX for nested PostgreSQL field incorrect");
}),
TestCase("succeeds for nested SQLite field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))",
"CREATE INDEX for nested SQLite field incorrect");
})
])
]),
TestCase("Insert succeeds", () =>
{
Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct");
@@ -226,70 +333,30 @@ public static class CommonCSharpTests
TestCase("Save succeeds", () =>
{
Expect.equal(Query.Save("tbl"),
$"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data",
"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data",
"INSERT ON CONFLICT UPDATE statement not correct");
}),
TestCase("Count succeeds", () =>
{
Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct");
}),
TestCase("Exists succeeds", () =>
{
Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it",
"Exists query not correct");
}),
TestCase("Find succeeds", () =>
{
Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct");
}),
TestCase("Update succeeds", () =>
{
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id",
"UPDATE full statement not correct");
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct");
}),
TestList("Count", new[]
TestCase("Delete succeeds", () =>
{
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", Field.EQ("thatField", 0)),
"SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field",
"JSON field text comparison count query not correct");
Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete 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", Field.LT("Test", 0)),
"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", Field.GE("Golf", 0)),
"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", Field.NEX("gone")),
"DELETE FROM tbl WHERE data ->> 'gone' IS NULL",
"DELETE by JSON comparison query not correct");
})
})
})
});
])
]);
}

View File

@@ -31,10 +31,10 @@ public class PostgresCSharpExtensionTests
/// Integration tests for the SQLite extension methods
/// </summary>
[Tests]
public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[]
{
TestList("CustomList", new[]
{
public static readonly Test Integration = TestList("Postgres.C#.Extensions",
[
TestList("CustomList",
[
TestCase("succeeds when data is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -57,9 +57,9 @@ public class PostgresCSharpExtensionTests
Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("CustomSingle", new[]
{
]),
TestList("CustomSingle",
[
TestCase("succeeds when a row is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -81,9 +81,9 @@ public class PostgresCSharpExtensionTests
new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("CustomNonQuery", new[]
{
]),
TestList("CustomNonQuery",
[
TestCase("succeeds when operating on data", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -107,7 +107,7 @@ public class PostgresCSharpExtensionTests
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();
@@ -169,8 +169,8 @@ public class PostgresCSharpExtensionTests
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestList("Insert", new[]
{
TestList("Insert",
[
TestCase("succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -198,9 +198,9 @@ public class PostgresCSharpExtensionTests
// This is what should have happened
}
})
}),
TestList("save", new[]
{
]),
TestList("save",
[
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -230,7 +230,7 @@ public class PostgresCSharpExtensionTests
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();
@@ -240,6 +240,7 @@ public class PostgresCSharpExtensionTests
var theCount = await conn.CountAll(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents");
}),
#pragma warning disable CS0618
TestCase("CountByField succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -249,6 +250,7 @@ public class PostgresCSharpExtensionTests
var theCount = await conn.CountByField(PostgresDb.TableName, Field.EQ("Value", "purple"));
Expect.equal(theCount, 2, "There should have been 2 matching documents");
}),
#pragma warning restore CS0618
TestCase("CountByContains succeeds", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -267,8 +269,8 @@ public class PostgresCSharpExtensionTests
var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)");
Expect.equal(theCount, 3, "There should have been 3 matching documents");
}),
TestList("ExistsById", new[]
{
TestList("ExistsById",
[
TestCase("succeeds when a document exists", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -287,9 +289,10 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsById(PostgresDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ExistsByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ExistsByField",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -308,9 +311,10 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EQ("NumValue", "six"));
Expect.isFalse(exists, "There should not have been existing documents");
})
}),
TestList("ExistsByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("ExistsByContains",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -329,9 +333,9 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" });
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("ExistsByJsonPath", new[]
{
]),
TestList("ExistsByJsonPath",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -350,9 +354,9 @@ public class PostgresCSharpExtensionTests
var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)");
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("FindAll", new[]
{
]),
TestList("FindAll",
[
TestCase("succeeds when there is data", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -372,9 +376,9 @@ public class PostgresCSharpExtensionTests
var results = await conn.FindAll<JsonDocument>(PostgresDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("FindById", new[]
{
]),
TestList("FindById",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -394,9 +398,10 @@ public class PostgresCSharpExtensionTests
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[]
{
]),
#pragma warning disable CS0618
TestList("FindByField",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -415,9 +420,10 @@ public class PostgresCSharpExtensionTests
var docs = await conn.FindByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("FindByContains",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -437,9 +443,9 @@ public class PostgresCSharpExtensionTests
var docs = await conn.FindByContains<JsonDocument>(PostgresDb.TableName, new { Value = "mauve" });
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindByJsonPath", new[]
{
]),
TestList("FindByJsonPath",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -458,9 +464,10 @@ public class PostgresCSharpExtensionTests
var docs = await conn.FindByJsonPath<JsonDocument>(PostgresDb.TableName, "$.NumValue ? (@ < 0)");
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindFirstByField", new[]
{
]),
#pragma warning disable CS0618
TestList("FindFirstByField",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -490,9 +497,10 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByField<JsonDocument>(PostgresDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("FindFirstByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("FindFirstByContains",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -523,9 +531,9 @@ public class PostgresCSharpExtensionTests
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[]
{
]),
TestList("FindFirstByJsonPath",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -557,9 +565,9 @@ public class PostgresCSharpExtensionTests
var doc = await conn.FindFirstByJsonPath<JsonDocument>(PostgresDb.TableName, "$.Id ? (@ == \"nope\")");
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("UpdateById", new[]
{
]),
TestList("UpdateById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -588,9 +596,9 @@ public class PostgresCSharpExtensionTests
await conn.UpdateById(PostgresDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("UpdateByFunc", new[]
{
]),
TestList("UpdateByFunc",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -617,9 +625,9 @@ public class PostgresCSharpExtensionTests
await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
TestList("PatchById", new[]
{
]),
TestList("PatchById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -641,9 +649,10 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("PatchByField", new[]
{
]),
#pragma warning disable CS0618
TestList("PatchByField",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -664,9 +673,10 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
})
}),
TestList("PatchByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("PatchByContains",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -687,9 +697,9 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" });
})
}),
TestList("PatchByJsonPath", new[]
{
]),
TestList("PatchByJsonPath",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -710,9 +720,9 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" });
})
}),
TestList("RemoveFieldsById", new[]
{
]),
TestList("RemoveFieldsById",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -754,9 +764,10 @@ public class PostgresCSharpExtensionTests
// This not raising an exception is the test
await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Value" });
})
}),
TestList("RemoveFieldsByField", new[]
{
]),
#pragma warning disable CS0618
TestList("RemoveFieldsByField",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -800,9 +811,10 @@ public class PostgresCSharpExtensionTests
await conn.RemoveFieldsByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"),
new[] { "Value" });
})
}),
TestList("RemoveFieldsByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("RemoveFieldsByContains",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -846,9 +858,9 @@ public class PostgresCSharpExtensionTests
await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" },
new[] { "Value" });
})
}),
TestList("RemoveFieldsByJsonPath", new[]
{
]),
TestList("RemoveFieldsByJsonPath",
[
TestCase("succeeds when multiple fields are removed", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -892,9 +904,9 @@ public class PostgresCSharpExtensionTests
await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")",
new[] { "Value" });
})
}),
TestList("DeleteById", new[]
{
]),
TestList("DeleteById",
[
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -915,9 +927,10 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByField", new[]
{
]),
#pragma warning disable CS0618
TestList("DeleteByField",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -938,9 +951,10 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByContains", new[]
{
]),
#pragma warning restore CS0618
TestList("DeleteByContains",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -961,9 +975,9 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByJsonPath", new[]
{
]),
TestList("DeleteByJsonPath",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = PostgresDb.BuildDb();
@@ -984,6 +998,6 @@ public class PostgresCSharpExtensionTests
var remaining = await conn.CountAll(PostgresDb.TableName);
Expect.equal(remaining, 5, "There should have been 5 documents remaining");
})
}),
});
]),
]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
using BitBadger.Documents.Postgres;
using Npgsql;
using Npgsql.FSharp;
using ThrowawayDb.Postgres;
@@ -53,65 +54,55 @@ public static class PostgresDb
/// The host for the database
/// </summary>
private static readonly Lazy<string> DbHost = new(() =>
{
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbHost") switch
Environment.GetEnvironmentVariable("BBDOX_PG_HOST") 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
Environment.GetEnvironmentVariable("BBDOX_PG_PORT") 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
Environment.GetEnvironmentVariable("BBDOX_PG_DATABASE") 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
Environment.GetEnvironmentVariable("BBDOX_PG_USER") 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
Environment.GetEnvironmentVariable("BBDOX_PG_PWD") switch
{
null => "postgres",
var pwd when pwd.Trim() == "" => "postgres",
var pwd => pwd
};
});
/// <summary>
@@ -141,7 +132,7 @@ public static class PostgresDb
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));
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName, Dialect.PostgreSQL), sqlProps));
Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString));

View File

@@ -18,10 +18,10 @@ public static class SqliteCSharpExtensionTests
/// Integration tests for the SQLite extension methods
/// </summary>
[Tests]
public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[]
{
TestList("CustomSingle", new[]
{
public static readonly Test Integration = TestList("Sqlite.C#.Extensions",
[
TestList("CustomSingle",
[
TestCase("succeeds when a row is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -43,9 +43,9 @@ public static class SqliteCSharpExtensionTests
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("CustomList", new[]
{
]),
TestList("CustomList",
[
TestCase("succeeds when data is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -67,9 +67,9 @@ public static class SqliteCSharpExtensionTests
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("CustomNonQuery", new[]
{
]),
TestList("CustomNonQuery",
[
TestCase("succeeds when operating on data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -93,7 +93,7 @@ public static class SqliteCSharpExtensionTests
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();
@@ -140,8 +140,8 @@ public static class SqliteCSharpExtensionTests
exists = await indexExists();
Expect.isTrue(exists, "The index should now exist");
}),
TestList("Insert", new[]
{
TestList("Insert",
[
TestCase("succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -168,9 +168,9 @@ public static class SqliteCSharpExtensionTests
// This is what is supposed to happen
}
})
}),
TestList("Save", new[]
{
]),
TestList("Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -203,7 +203,7 @@ public static class SqliteCSharpExtensionTests
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();
@@ -213,6 +213,7 @@ public static class SqliteCSharpExtensionTests
var theCount = await conn.CountAll(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents");
}),
#pragma warning disable CS0618
TestCase("CountByField succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -222,8 +223,9 @@ public static class SqliteCSharpExtensionTests
var theCount = await conn.CountByField(SqliteDb.TableName, Field.EQ("Value", "purple"));
Expect.equal(theCount, 2L, "There should have been 2 matching documents");
}),
TestList("ExistsById", new[]
{
#pragma warning restore CS0618
TestList("ExistsById",
[
TestCase("succeeds when a document exists", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -242,9 +244,10 @@ public static class SqliteCSharpExtensionTests
var exists = await conn.ExistsById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ExistsByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ExistsByField",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -263,9 +266,10 @@ public static class SqliteCSharpExtensionTests
var exists = await conn.ExistsByField(SqliteDb.TableName, Field.EQ("Nothing", "none"));
Expect.isFalse(exists, "There should not have been any existing documents");
})
}),
TestList("FindAll", new[]
{
]),
#pragma warning restore CS0618
TestList("FindAll",
[
TestCase("succeeds when there is data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -285,9 +289,9 @@ public static class SqliteCSharpExtensionTests
var results = await conn.FindAll<JsonDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("FindById", new[]
{
]),
TestList("FindById",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -307,9 +311,10 @@ public static class SqliteCSharpExtensionTests
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[]
{
]),
#pragma warning disable CS0618
TestList("FindByField",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -328,9 +333,9 @@ public static class SqliteCSharpExtensionTests
var docs = await conn.FindByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FindFirstByField", new[]
{
]),
TestList("FindFirstByField",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -360,9 +365,10 @@ public static class SqliteCSharpExtensionTests
var doc = await conn.FindFirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("UpdateById", new[]
{
]),
#pragma warning restore CS0618
TestList("UpdateById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -389,9 +395,9 @@ public static class SqliteCSharpExtensionTests
await conn.UpdateById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("UpdateByFunc", new[]
{
]),
TestList("UpdateByFunc",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -418,9 +424,9 @@ public static class SqliteCSharpExtensionTests
await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
TestList("PatchById", new[]
{
]),
TestList("PatchById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -443,9 +449,10 @@ public static class SqliteCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("PatchByField", new[]
{
]),
#pragma warning disable CS0618
TestList("PatchByField",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -466,9 +473,10 @@ public static class SqliteCSharpExtensionTests
// This not raising an exception is the test
await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
})
}),
TestList("RemoveFieldsById", new[]
{
]),
#pragma warning restore CS0618
TestList("RemoveFieldsById",
[
TestCase("succeeds when fields are removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -498,9 +506,10 @@ public static class SqliteCSharpExtensionTests
// This not raising an exception is the test
await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Value" });
})
}),
TestList("RemoveFieldsByField", new[]
{
]),
#pragma warning disable CS0618
TestList("RemoveFieldsByField",
[
TestCase("succeeds when a field is removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -529,9 +538,10 @@ public static class SqliteCSharpExtensionTests
// This not raising an exception is the test
await conn.RemoveFieldsByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" });
})
}),
TestList("DeleteById", new[]
{
]),
#pragma warning restore CS0618
TestList("DeleteById",
[
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -552,9 +562,10 @@ public static class SqliteCSharpExtensionTests
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestList("DeleteByField", new[]
{
]),
#pragma warning disable CS0618
TestList("DeleteByField",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -575,7 +586,8 @@ public static class SqliteCSharpExtensionTests
var remaining = await conn.CountAll(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
]),
#pragma warning restore CS0618
TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:"))
});
]);
}

View File

@@ -1,5 +1,4 @@
using System.Text.Json;
using Expecto.CSharp;
using Expecto.CSharp;
using Expecto;
using Microsoft.Data.Sqlite;
using Microsoft.FSharp.Core;
@@ -17,49 +16,86 @@ public static class SqliteCSharpTests
/// <summary>
/// Unit tests for the SQLite library
/// </summary>
private static readonly Test Unit = TestList("Unit", new[]
private static readonly Test Unit = TestList("Unit",
[
TestList("Query",
[
TestList("WhereByFields",
[
TestCase("succeeds for a single field when a logical operator is passed", () =>
{
TestList("Query", new[]
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.Any,
[Field.GT("theField", 0).WithParameterName("@test")]),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when an existence operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]),
"data->>'thatField' IS NULL", "WHERE clause not correct");
}),
TestCase("succeeds for a single field when a between operator is passed", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.BT("aField", 50, 99).WithParameterName("@range")]),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with logical operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]),
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct");
}),
TestCase("succeeds for any multiple fields with an existence operator", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]),
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct");
}),
TestCase("succeeds for all multiple fields with between operators", () =>
{
Expect.equal(
Sqlite.Query.WhereByFields(FieldMatch.All,
[Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]),
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max",
"WHERE clause not correct");
})
]),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
}),
TestCase("Patch succeeds", () =>
{
Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName),
$"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct");
}),
TestCase("RemoveFields succeeds", () =>
{
Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]),
$"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)",
"Field removal query not correct");
}),
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id",
"By-ID query not correct");
}),
TestCase("ByFields succeeds", () =>
{
Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]),
"unit WHERE data->>'That' > @field0", "By-Field query not correct");
}),
TestCase("Definition.EnsureTable succeeds", () =>
{
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", Field.NE("Part", 0)),
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field",
"UPDATE partial by JSON comparison query not correct");
})
}),
TestList("RemoveFields", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }),
"UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id",
"Remove field by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0),
new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }),
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data ->> 'Fly' < @field",
"Remove field by field query not correct");
})
})
}),
TestList("Parameters", new[]
{
]),
TestList("Parameters",
[
TestCase("Id succeeds", () =>
{
var theParam = Parameters.Id(7);
@@ -72,10 +108,10 @@ public static class SqliteCSharpTests
Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect");
Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect");
}),
#pragma warning disable CS0618
TestCase("AddField succeeds when adding a parameter", () =>
{
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), Enumerable.Empty<SqliteParameter>())
.ToList();
var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList();
Expect.hasLength(paramList, 1, "There should have been a parameter added");
var theParam = paramList[0];
Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect");
@@ -83,25 +119,26 @@ public static class SqliteCSharpTests
}),
TestCase("AddField succeeds when not adding a parameter", () =>
{
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), Enumerable.Empty<SqliteParameter>());
var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []);
Expect.isEmpty(paramSeq, "There should not have been any parameters added");
}),
#pragma warning restore CS0618
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()
{
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
};
];
/// <summary>
/// Add the test documents to the database
@@ -111,8 +148,8 @@ public static class SqliteCSharpTests
foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc);
}
private static readonly Test Integration = TestList("Integration", new[]
{
private static readonly Test Integration = TestList("Integration",
[
TestCase("Configuration.UseConnectionString succeeds", () =>
{
try
@@ -126,10 +163,10 @@ public static class SqliteCSharpTests
Sqlite.Configuration.UseConnectionString("Data Source=:memory:");
}
}),
TestList("Custom", new[]
{
TestList("Single", new[]
{
TestList("Custom",
[
TestList("Single",
[
TestCase("succeeds when a row is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -149,9 +186,9 @@ public static class SqliteCSharpTests
new[] { Parameters.Id("eighty") }, Results.FromData<JsonDocument>);
Expect.isNull(doc, "There should not have been a document returned");
})
}),
TestList("List", new[]
{
]),
TestList("List",
[
TestCase("succeeds when data is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -171,9 +208,9 @@ public static class SqliteCSharpTests
new[] { new SqliteParameter("@value", 100) }, Results.FromData<JsonDocument>);
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("NonQuery", new[]
{
]),
TestList("NonQuery",
[
TestCase("succeeds when operating on data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -195,7 +232,7 @@ public static class SqliteCSharpTests
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();
@@ -203,9 +240,9 @@ public static class SqliteCSharpTests
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[]
{
]),
TestList("Definition",
[
TestCase("EnsureTable succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -233,21 +270,23 @@ public static class SqliteCSharpTests
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();
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();
exists = await IndexExists();
Expect.isTrue(exists, "The index should now exist");
return;
Task<bool> IndexExists() => Custom.Scalar(
$"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it",
Parameters.None, Results.ToExists);
})
}),
TestList("Document.Insert", new[]
{
]),
TestList("Document.Insert",
[
TestCase("succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -272,9 +311,9 @@ public static class SqliteCSharpTests
// This is what is supposed to happen
}
})
}),
TestList("Document.Save", new[]
{
]),
TestList("Document.Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -305,9 +344,9 @@ public static class SqliteCSharpTests
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[]
{
]),
TestList("Count",
[
TestCase("All succeeds", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -316,19 +355,29 @@ public static class SqliteCSharpTests
var theCount = await Count.All(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents");
}),
TestCase("ByField succeeds", async () =>
#pragma warning disable CS0618
TestCase("ByField succeeds for numeric range", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(SqliteDb.TableName, Field.EQ("Value", "purple"));
Expect.equal(theCount, 2L, "There should have been 2 matching documents");
})
var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 3L, "There should have been 3 matching documents");
}),
TestList("Exists", new[]
{
TestList("ById", new[]
TestCase("ByField succeeds for non-numeric range", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple"));
Expect.equal(theCount, 1L, "There should have been 1 matching document");
})
#pragma warning restore CS0618
]),
TestList("Exists",
[
TestList("ById",
[
TestCase("succeeds when a document exists", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -345,9 +394,10 @@ public static class SqliteCSharpTests
var exists = await Exists.ById(SqliteDb.TableName, "seven");
Expect.isFalse(exists, "There should not have been an existing document");
})
}),
TestList("ByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ByField",
[
TestCase("succeeds when documents exist", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -364,12 +414,13 @@ public static class SqliteCSharpTests
var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none"));
Expect.isFalse(exists, "There should not have been any existing documents");
})
})
}),
TestList("Find", new[]
{
TestList("All", new[]
{
])
#pragma warning restore CS0618
]),
TestList("Find",
[
TestList("All",
[
TestCase("succeeds when there is data", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -387,9 +438,9 @@ public static class SqliteCSharpTests
var results = await Find.All<SubDocument>(SqliteDb.TableName);
Expect.isEmpty(results, "There should have been no documents returned");
})
}),
TestList("ById", new[]
{
]),
TestList("ById",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -407,9 +458,10 @@ public static class SqliteCSharpTests
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[]
{
]),
#pragma warning disable CS0618
TestList("ByField",
[
TestCase("succeeds when documents are found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -426,9 +478,9 @@ public static class SqliteCSharpTests
var docs = await Find.ByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "mauve"));
Expect.isEmpty(docs, "There should have been no documents returned");
})
}),
TestList("FirstByField", new[]
{
]),
TestList("FirstByField",
[
TestCase("succeeds when a document is found", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -455,12 +507,13 @@ public static class SqliteCSharpTests
var doc = await Find.FirstByField<JsonDocument>(SqliteDb.TableName, Field.EQ("Value", "absent"));
Expect.isNull(doc, "There should not have been a document returned");
})
})
}),
TestList("Update", new[]
{
TestList("ById", new[]
{
])
#pragma warning restore CS0618
]),
TestList("Update",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -486,9 +539,9 @@ public static class SqliteCSharpTests
await Update.ById(SqliteDb.TableName, "test",
new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } });
})
}),
TestList("ByFunc", new[]
{
]),
TestList("ByFunc",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -514,12 +567,12 @@ public static class SqliteCSharpTests
await Update.ByFunc(SqliteDb.TableName, doc => doc.Id,
new JsonDocument { Id = "one", Value = "le un", NumValue = 1 });
})
}),
}),
TestList("Patch", new[]
{
TestList("ById", new[]
{
]),
]),
TestList("Patch",
[
TestList("ById",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -541,9 +594,10 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" });
})
}),
TestList("ByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ByField",
[
TestCase("succeeds when a document is updated", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -563,12 +617,13 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" });
})
})
}),
TestList("RemoveFields", new[]
{
TestList("ById", new[]
{
])
#pragma warning restore CS0618
]),
TestList("RemoveFields",
[
TestList("ById",
[
TestCase("succeeds when fields are removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -595,9 +650,10 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" });
})
}),
TestList("ByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ByField",
[
TestCase("succeeds when a field is removed", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -623,12 +679,13 @@ public static class SqliteCSharpTests
// This not raising an exception is the test
await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" });
})
})
}),
TestList("Delete", new[]
{
TestList("ById", new[]
{
])
#pragma warning restore CS0618
]),
TestList("Delete",
[
TestList("ById",
[
TestCase("succeeds when a document is deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -647,9 +704,10 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
}),
TestList("ByField", new[]
{
]),
#pragma warning disable CS0618
TestList("ByField",
[
TestCase("succeeds when documents are deleted", async () =>
{
await using var db = await SqliteDb.BuildDb();
@@ -668,14 +726,15 @@ public static class SqliteCSharpTests
var remaining = await Count.All(SqliteDb.TableName);
Expect.equal(remaining, 5L, "There should have been 5 documents remaining");
})
})
}),
])
#pragma warning restore CS0618
]),
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) });
public static readonly Test All = TestList("Sqlite.C#", [Unit, TestSequenced(Integration)]);
}

View File

@@ -15,7 +15,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" />
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -28,6 +28,9 @@ let all =
test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct"
}
test "BT succeeds" {
Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct"""
}
test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct"""
}
@@ -35,27 +38,149 @@ let all =
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"
testList "Field" [
test "EQ succeeds" {
let field = Field.EQ "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Op EQ "Operator incorrect"
Expect.equal field.Value 14 "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct"
test "GT succeeds" {
let field = Field.GT "Great" "night"
Expect.equal field.Name "Great" "Field name incorrect"
Expect.equal field.Op GT "Operator incorrect"
Expect.equal field.Value "night" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
testList "whereByField" [
test "succeeds when a logical operator is passed" {
test "GE succeeds" {
let field = Field.GE "Nice" 88L
Expect.equal field.Name "Nice" "Field name incorrect"
Expect.equal field.Op GE "Operator incorrect"
Expect.equal field.Value 88L "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "LT succeeds" {
let field = Field.LT "Lesser" "seven"
Expect.equal field.Name "Lesser" "Field name incorrect"
Expect.equal field.Op LT "Operator incorrect"
Expect.equal field.Value "seven" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "LE succeeds" {
let field = Field.LE "Nobody" "KNOWS";
Expect.equal field.Name "Nobody" "Field name incorrect"
Expect.equal field.Op LE "Operator incorrect"
Expect.equal field.Value "KNOWS" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NE succeeds" {
let field = Field.NE "Park" "here"
Expect.equal field.Name "Park" "Field name incorrect"
Expect.equal field.Op NE "Operator incorrect"
Expect.equal field.Value "here" "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "BT succeeds" {
let field = Field.BT "Age" 18 49
Expect.equal field.Name "Age" "Field name incorrect"
Expect.equal field.Op BT "Operator incorrect"
Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "EX succeeds" {
let field = Field.EX "Groovy"
Expect.equal field.Name "Groovy" "Field name incorrect"
Expect.equal field.Op EX "Operator incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "NEX succeeds" {
let field = Field.NEX "Rad"
Expect.equal field.Name "Rad" "Field name incorrect"
Expect.equal field.Op NEX "Operator incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
test "WithParameterName succeeds" {
let field = (Field.EQ "Bob" "Tom").WithParameterName "@name"
Expect.isSome field.ParameterName "The parameter name should have been filled"
Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect"
}
test "WithQualifier succeeds" {
let field = (Field.EQ "Bill" "Matt").WithQualifier "joe"
Expect.isSome field.Qualifier "The table qualifier should have been filled"
Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
}
testList "Path" [
test "succeeds for a PostgreSQL single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal
(Query.whereByField (Field.GT "theField" 0) "@test")
"data ->> 'theField' > @test"
"WHERE clause not correct"
"this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds when an existence operator is passed" {
Expect.equal
(Query.whereByField (Field.NEX "thatField") "")
"data ->> 'thatField' IS NULL"
"WHERE clause not correct"
test "succeeds for a PostgreSQL nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a SQLite single field with no qualifier" {
let field = Field.GE "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with a qualifier" {
let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect"
}
]
]
testList "FieldMatch.ToString" [
test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
testList "Query" [
test "statementWhere succeeds" {
Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly"
}
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
@@ -66,25 +191,40 @@ let all =
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table")
(Query.Definition.ensureKey "test.table" PostgreSQL)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table")
(Query.Definition.ensureKey "table" SQLite)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
test "ensureIndexOn succeeds for multiple fields and directions" {
testList "ensureIndexOn" [
test "succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ])
(Query.Definition.ensureIndexOn
"test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL)
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
"((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ]
|> String.concat "")
"CREATE INDEX for multiple field statement incorrect"
}
test "succeeds for nested PostgreSQL field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))"
"CREATE INDEX for nested PostgreSQL field incorrect"
}
test "succeeds for nested SQLite field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))"
"CREATE INDEX for nested SQLite field incorrect"
}
]
]
test "insert succeeds" {
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct"
@@ -95,65 +235,23 @@ let all =
$"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
test "count succeeds" {
Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "exists succeeds" {
Expect.equal
(Query.exists tbl "turkey")
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it"
"Exists query not correct"
}
test "find succeeds" {
Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct"
}
test "update succeeds" {
Expect.equal
(Query.update tbl)
$"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id"
"UPDATE full statement not correct"
Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query 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 (Field.EQ "thatField" 0))
$"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 (Field.LT "Test" 0))
$"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 (Field.GE "Golf" 0))
$"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 (Field.NEX "gone"))
$"DELETE FROM {tbl} WHERE data ->> 'gone' IS NULL"
"DELETE by JSON comparison query not correct"
test "delete succeeds" {
Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct"
}
]
]
]

View File

@@ -7,6 +7,8 @@ open Expecto
open Npgsql
open Types
#nowarn "0044"
/// Open a connection to the throwaway database
let private mkConn (db: ThrowawayPostgresDb) =
let conn = new NpgsqlConnection(db.ConnectionString)

View File

@@ -5,41 +5,212 @@ open BitBadger.Documents
open BitBadger.Documents.Postgres
open BitBadger.Documents.Tests
#nowarn "0044"
/// 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"
testList "idParam" [
// NOTE: these tests also exercise all branches of the internal parameterFor function
test "succeeds for byte ID" {
Expect.equal
(idParam (sbyte 7)) ("@id", Sql.int8 (sbyte 7)) "Byte ID parameter not constructed correctly"
}
test "succeeds for unsigned byte ID" {
Expect.equal
(idParam (byte 7))
("@id", Sql.int8 (int8 (byte 7)))
"Unsigned byte ID parameter not constructed correctly"
}
test "succeeds for short ID" {
Expect.equal
(idParam (int16 44))
("@id", Sql.int16 (int16 44))
"Short ID parameter not constructed correctly"
}
test "succeeds for unsigned short ID" {
Expect.equal
(idParam (uint16 64))
("@id", Sql.int16 (int16 64))
"Unsigned short ID parameter not constructed correctly"
}
test "succeeds for integer ID" {
Expect.equal (idParam 88) ("@id", Sql.int 88) "Int ID parameter not constructed correctly"
}
test "succeeds for unsigned integer ID" {
Expect.equal
(idParam (uint 889)) ("@id", Sql.int 889) "Unsigned int ID parameter not constructed correctly"
}
test "succeeds for long ID" {
Expect.equal
(idParam (int64 123))
("@id", Sql.int64 (int64 123))
"Long ID parameter not constructed correctly"
}
test "succeeds for unsigned long ID" {
Expect.equal
(idParam (uint64 6464))
("@id", Sql.int64 (int64 6464))
"Unsigned long ID parameter not constructed correctly"
}
test "succeeds for decimal ID" {
Expect.equal
(idParam (decimal 4.56))
("@id", Sql.decimal (decimal 4.56))
"Decimal ID parameter not constructed correctly"
}
test "succeeds for single ID" {
Expect.equal
(idParam (single 5.67))
("@id", Sql.double (double (single 5.67)))
"Single ID parameter not constructed correctly"
}
test "succeeds for double ID" {
Expect.equal
(idParam (double 6.78))
("@id", Sql.double (double 6.78))
"Double ID parameter not constructed correctly"
}
test "succeeds for string ID" {
Expect.equal (idParam "99") ("@id", Sql.string "99") "String ID parameter not constructed correctly"
}
test "succeeds for non-numeric non-string ID" {
let target = { new obj() with override _.ToString() = "ToString was called" }
Expect.equal
(idParam target)
("@id", Sql.string "ToString was called")
"Non-numeric, non-string parameter not constructed correctly"
}
]
test "jsonParam succeeds" {
Expect.equal
(jsonParam "@test" {| Something = "good" |})
("@test", Sql.jsonb """{"Something":"good"}""")
"JSON parameter not constructed correctly"
}
testList "addFieldParam" [
testList "addFieldParams" [
test "succeeds when a parameter is added" {
let paramList = addFieldParam "@field" (Field.EQ "it" "242") []
let paramList = addFieldParams [ Field.EQ "it" "242" ] []
Expect.hasLength paramList 1 "There should have been a parameter added"
let it = paramList[0]
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"
let name, value = Seq.head paramList
Expect.equal name "@field0" "Field parameter name not correct"
Expect.equal value (Sql.string "242") "Parameter value not correct"
}
test "succeeds when multiple independent parameters are added" {
let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ]
Expect.hasLength paramList 3 "There should have been 2 parameters added"
let p = Array.ofSeq paramList
Expect.equal (fst p[0]) "@id" "First field parameter name not correct"
Expect.equal (snd p[0]) (Sql.int 14) "First parameter value not correct"
Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct"
Expect.equal (snd p[1]) (Sql.string "you") "Second parameter value not correct"
Expect.equal (fst p[2]) "@field1" "Third parameter name not correct"
Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct"
}
test "succeeds when a parameter is not added" {
let paramList = addFieldParam "@field" (Field.EX "tacos") []
let paramList = addFieldParams [ Field.EX "tacos" ] []
Expect.isEmpty paramList "There should not have been any parameters added"
}
test "succeeds when two parameters are added for one field" {
let paramList =
addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] []
Expect.hasLength paramList 2 "There should have been 2 parameters added"
let name, value = Seq.head paramList
Expect.equal name "@testmin" "Minimum field name not correct"
Expect.equal value (Sql.string "eh") "Minimum parameter value not correct"
let name, value = paramList |> Seq.skip 1 |> Seq.head
Expect.equal name "@testmax" "Maximum field name not correct"
Expect.equal value (Sql.string "zed") "Maximum parameter value not correct"
}
]
testList "fieldNameParams" [
test "succeeds for one name" {
let name, value = fieldNameParams [ "bob" ]
Expect.equal name "@name" "The parameter name was incorrect"
Expect.equal value (Sql.string "bob") "The parameter value was incorrect"
}
test "succeeds for multiple names" {
let name, value = fieldNameParams [ "bob"; "tom"; "mike" ]
Expect.equal name "@name" "The parameter name was incorrect"
Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect"
}
]
testList "fieldNameParam" [
test "succeeds for one name" {
let name, value = fieldNameParam [ "bob" ]
Expect.equal name "@name" "The parameter name was incorrect"
Expect.equal value (Sql.string "bob") "The parameter value was incorrect"
}
test "succeeds for multiple names" {
let name, value = fieldNameParam [ "bob"; "tom"; "mike" ]
Expect.equal name "@name" "The parameter name was incorrect"
Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect"
}
]
test "noParams succeeds" {
Expect.isEmpty noParams "The no-params sequence should be empty"
}
]
testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields Any [ { Field.GT "theField" "0" with ParameterName = Some "@test" } ])
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields Any [ Field.NEX "thatField" ])
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed with numeric values" {
Expect.equal
(Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ])
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed with non-numeric values" {
Expect.equal
(Query.whereByFields Any [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ])
"data->>'field0' BETWEEN @alphamin AND @alphamax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ])
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ])
"data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ])
"(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
]
testList "whereById" [
test "succeeds for numeric ID" {
Expect.equal (Query.whereById 18) "(data->>'Id')::numeric = @id" "WHERE clause not correct"
}
test "succeeds for string ID" {
Expect.equal (Query.whereById "18") "data->>'Id' = @id" "WHERE clause not correct"
}
test "succeeds for non-numeric non-string ID" {
Expect.equal
(Query.whereById (System.Uri "https://example.com"))
"data->>'Id' = @id"
"WHERE clause not correct"
}
]
testList "Definition" [
test "ensureTable succeeds" {
Expect.equal
@@ -67,112 +238,34 @@ let unitTests =
test "whereJsonPathMatches succeeds" {
Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
}
testList "Count" [
test "byContains succeeds" {
test "patch succeeds" {
Expect.equal
(Query.Count.byContains PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria"
"JSON containment count query not correct"
(Query.patch PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data"
"Patch query not correct"
}
test "byJsonPath succeeds" {
test "removeFields 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"
(Query.removeFields PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data - @name"
"Field removal 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"
Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct"
}
test "byField succeeds" {
test "byFields succeeds" {
Expect.equal
(Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0))
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field"
"UPDATE partial by ID statement not correct"
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE (data->>'That')::numeric > @field0"
"By-Field query not correct"
}
test "byContains succeeds" {
Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct"
}
test "byPathMach 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"
(Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query 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 "RemoveFields" [
test "byId succeeds" {
Expect.equal
(Query.RemoveFields.byId "tbl")
"UPDATE tbl SET data = data - @name WHERE data ->> 'Id' = @id"
"Remove field by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0))
"UPDATE tbl SET data = data - @name WHERE data ->> 'Fly' < @field"
"Remove field by field query not correct"
}
test "byContains succeeds" {
Expect.equal
(Query.RemoveFields.byContains "tbl")
"UPDATE tbl SET data = data - @name WHERE data @> @criteria"
"Remove field by contains query not correct"
}
test "byJsonPath succeeds" {
Expect.equal
(Query.RemoveFields.byJsonPath "tbl")
"UPDATE tbl SET data = data - @name WHERE data @? @path::jsonpath"
"Remove field by JSON path query 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"
}
]
]
]
@@ -387,13 +480,39 @@ let integrationTests =
let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents"
}
testTask "byField succeeds" {
testList "byFields" [
testTask "succeeds when items are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName (Field.EQ "Value" "purple")
Expect.equal theCount 2 "There should have been 2 matching documents"
let! theCount =
Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ]
Expect.equal theCount 3 "There should have been 3 matching documents"
}
testTask "succeeds when items are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ]
Expect.equal theCount 0 "There should have been no matching documents"
}
]
testList "byField" [
testTask "succeeds for numeric range" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 3 "There should have been 3 matching documents"
}
testTask "succeeds for non-numeric range" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName (Field.BT "Value" "aardvark" "apple")
Expect.equal theCount 1 "There should have been 1 matching document"
}
]
testTask "byContains succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
@@ -426,6 +545,23 @@ let integrationTests =
Expect.isFalse exists "There should not have been an existing document"
}
]
testList "byFields" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ]
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists =
Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ]
Expect.isFalse exists "There should not have been existing documents"
}
]
testList "byField" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
@@ -515,6 +651,26 @@ let integrationTests =
Expect.isNone doc "There should not have been a document returned"
}
]
testList "byFields" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFields<JsonDocument>
PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ]
Expect.equal (List.length docs) 1 "There should have been one document returned"
}
testTask "succeeds when documents are not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFields<JsonDocument>
PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ]
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "byField" [
testTask "succeeds when documents are found" {
use db = PostgresDb.BuildDb()

View File

@@ -1,19 +1,27 @@
open Expecto
open BitBadger.Documents.Tests.CSharp
let postgresOnly =
match System.Environment.GetEnvironmentVariable "BBDOX_PG_ONLY" with
| null -> false
| "true" -> true
| _ -> false
let allTests =
testList
"BitBadger.Documents"
[ CommonTests.all
testList "BitBadger.Documents" [
if not postgresOnly then
CommonTests.all
CommonCSharpTests.Unit
PostgresTests.all
PostgresCSharpTests.All
PostgresExtensionTests.integrationTests
testSequenced PostgresCSharpExtensionTests.Integration
if not postgresOnly then
SqliteTests.all
SqliteCSharpTests.All
SqliteExtensionTests.integrationTests
testSequenced SqliteCSharpExtensionTests.Integration ]
testSequenced SqliteCSharpExtensionTests.Integration
]
[<EntryPoint>]
let main args = runTestsWithCLIArgs [] args allTests

View File

@@ -8,6 +8,8 @@ open Expecto
open Microsoft.Data.Sqlite
open Types
#nowarn "0044"
/// Integration tests for the F# extensions on the SqliteConnection data type
let integrationTests =
let loadDocs () = backgroundTask {

View File

@@ -8,47 +8,80 @@ open Expecto
open Microsoft.Data.Sqlite
open Types
#nowarn "0044"
/// Unit tests for the SQLite library
let unitTests =
testList "Unit" [
testList "Query" [
testList "whereByFields" [
test "succeeds for a single field when a logical operator is passed" {
Expect.equal
(Query.whereByFields Any [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ])
"data->>'theField' > @test"
"WHERE clause not correct"
}
test "succeeds for a single field when an existence operator is passed" {
Expect.equal
(Query.whereByFields Any [ Field.NEX "thatField" ])
"data->>'thatField' IS NULL"
"WHERE clause not correct"
}
test "succeeds for a single field when a between operator is passed" {
Expect.equal
(Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ])
"data->>'aField' BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with logical operators" {
Expect.equal
(Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ])
"data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1"
"WHERE clause not correct"
}
test "succeeds for any multiple fields with an existence operator" {
Expect.equal
(Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ])
"data->>'thatField' IS NULL OR data->>'thisField' >= @field0"
"WHERE clause not correct"
}
test "succeeds for all multiple fields with between operators" {
Expect.equal
(Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ])
"data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max"
"WHERE clause not correct"
}
]
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct"
}
test "patch succeeds" {
Expect.equal
(Query.patch SqliteDb.TableName)
$"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))"
"Patch query not correct"
}
test "removeFields succeeds" {
Expect.equal
(Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ])
$"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)"
"Field removal query not correct"
}
test "byId succeeds" {
Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct"
}
test "byFields succeeds" {
Expect.equal
(Query.byFields "unit" Any [ Field.GT "That" 14 ])
"unit WHERE data->>'That' > @field0"
"By-Field query not correct"
}
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" (Field.NE "Part" 0))
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field"
"UPDATE partial by JSON comparison query not correct"
}
]
testList "RemoveFields" [
test "byId succeeds" {
Expect.equal
(Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ])
"UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id"
"Remove field by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.RemoveFields.byField
"tbl"
(Field.GT "Fly" 0)
[ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ])
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data ->> 'Fly' > @field"
"Remove field by field query not correct"
}
]
]
testList "Parameters" [
test "idParam succeeds" {
@@ -65,7 +98,7 @@ let unitTests =
test "succeeds when adding a parameter" {
let paramList = addFieldParam "@field" (Field.EQ "it" 99) []
Expect.hasLength paramList 1 "There should have been a parameter added"
let theParam = paramList[0]
let theParam = Seq.head paramList
Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect"
Expect.equal theParam.Value 99 "The parameter value is incorrect"
}
@@ -299,12 +332,19 @@ let integrationTests =
let! theCount = Count.all SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents"
}
testTask "byField succeeds" {
testTask "byField succeeds for a numeric range" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField SqliteDb.TableName (Field.EQ "Value" "purple")
Expect.equal theCount 2L "There should have been 2 matching documents"
let! theCount = Count.byField SqliteDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 3L "There should have been 3 matching documents"
}
testTask "byField succeeds for a non-numeric range" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField SqliteDb.TableName (Field.BT "Value" "aardvark" "apple")
Expect.equal theCount 1L "There should have been 1 matching document"
}
]
testList "Exists" [

13
src/package.sh Executable file
View File

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

33
src/test_all.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
dotnet clean BitBadger.Documents.sln
dotnet restore BitBadger.Documents.sln
dotnet build BitBadger.Documents.sln --no-restore
cd ./Tests || exit
export BBDOX_PG_PORT=8301
PG_VERSIONS=('12' '13' '14' '15' 'latest')
NET_VERSIONS=('6.0' '8.0')
for PG_VERSION in "${PG_VERSIONS[@]}"
do
echo Starting PostgreSQL:$PG_VERSION
docker run -d -p $BBDOX_PG_PORT:5432 --name pg_test -e POSTGRES_PASSWORD=postgres postgres:$PG_VERSION
sleep 4
for NET_VERSION in "${NET_VERSIONS[@]}"
do
if [ "$PG_VERSION" = "latest" ]; then
echo Testing SQLite and PostgreSQL under .NET $NET_VERSION...
dotnet run -f net$NET_VERSION
else
echo Testing PostgreSQL v$PG_VERSION under .NET $NET_VERSION...
BBDOX_PG_ONLY="true" dotnet run -f net$NET_VERSION
fi
done
docker stop pg_test
sleep 2
docker rm pg_test
done
cd .. || exit