14 Commits
v3-rc2 ... v3.1

Author SHA1 Message Date
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
22 changed files with 820 additions and 355 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,8 @@
<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>
<PackageReleaseNotes>v3 release</PackageReleaseNotes>
<PackageTags>JSON Document SQL</PackageTags>
</PropertyGroup>
@@ -12,7 +13,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,6 +30,7 @@ type Op =
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
@@ -68,11 +71,15 @@ type Field = {
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value }
/// Create a BETWEEN field criterion
static member BT name (min: obj) (max: obj) =
{ Name = name; Op = BT; Value = [ min; max ] }
/// Create an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj () }
/// 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 () }
@@ -150,17 +157,6 @@ module Query =
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
/// Queries to define tables and indexes
module Definition =
@@ -205,59 +201,3 @@ 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 =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Queries for determining document existence
module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{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"}"""

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,8 @@
<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>
<PackageReleaseNotes>v3 release; official replacement for BitBadger.Npgsql.Documents</PackageReleaseNotes>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
</PropertyGroup>
@@ -14,6 +15,7 @@
<ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@@ -63,17 +63,29 @@ module Parameters =
let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field")
/// Create a JSON field parameter
[<CompiledName "FSharpAddField">]
let addFieldParam name field parameters =
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
List.concat
[ parameters
[ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values)))
($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] ]
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) :: parameters
/// Create a JSON field parameter (name "@field")
/// Create a JSON field parameter
let AddField name field parameters =
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
ResizeArray
[ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values)))
($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ]
|> Seq.append parameters
| _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters
@@ -97,6 +109,25 @@ module Parameters =
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
let whereByField field paramName =
match field.Op with
| EX | NEX -> $"data->>'{field.Name}' {field.Op}"
| BT ->
let names = $"{paramName}min AND {paramName}max"
let values = field.Value :?> obj list
match values[0] with
| :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64
| :? decimal | :? single | :? double -> $"(data->>'{field.Name}')::numeric {field.Op} {names}"
| _ -> $"data->>'{field.Name}' {field.Op} {names}"
| _ -> $"data->>'{field.Name}' {field.Op} %s{paramName}"
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Table and index definition queries
module Definition =
@@ -112,6 +143,11 @@ module Query =
let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
/// Query to update a document
[<CompiledName "Update">]
let update tableName =
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition
[<CompiledName "WhereDataContains">]
let whereDataContains paramName =
@@ -125,6 +161,16 @@ module Query =
/// Queries for counting documents
module Count =
/// Query to count all documents in a table
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Query to count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
@@ -138,6 +184,16 @@ module Query =
/// Queries for determining document existence
module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Query to determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
@@ -151,6 +207,16 @@ module Query =
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Query to retrieve documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =
@@ -171,12 +237,12 @@ module Query =
/// Query to patch a document by its ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
whereById "@id" |> update tableName
/// Query to patch documents match a JSON field comparison (->> =)
[<CompiledName "ByField">]
let byField tableName field =
Query.whereByField field "@field" |> update tableName
whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">]
@@ -198,12 +264,12 @@ module Query =
/// Query to remove fields from a document by the document's ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
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
whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">]
@@ -218,6 +284,16 @@ module Query =
/// 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"}"""
/// Query to delete documents using a JSON containment query (@>)
[<CompiledName "ByContains">]
let byContains tableName =

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,8 @@
<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>
<PackageReleaseNotes>Overall v3 release; initial release supporting SQLite</PackageReleaseNotes>
<PackageTags>JSON Document SQLite</PackageTags>
</PropertyGroup>
@@ -13,7 +14,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

@@ -31,6 +31,21 @@ module Configuration =
[<RequireQualifiedAccess>]
module Query =
/// 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 -> ""
| BT -> $" {paramName}min AND {paramName}max"
| _ -> $" %s{paramName}"
$"data->>'{field.Name}' {field.Op}{theRest}"
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// Data definition
module Definition =
@@ -39,6 +54,50 @@ module Query =
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
/// Query to update a document
[<CompiledName "Update">]
let update tableName =
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
/// Queries for counting documents
module Count =
/// Query to count all documents in a table
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Queries for determining document existence
module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Document patching (partial update) queries
module Patch =
@@ -49,12 +108,12 @@ module Query =
/// Query to patch (partially update) a document by its ID
[<CompiledName "ById">]
let byId tableName =
Query.whereById "@id" |> update tableName
whereById "@id" |> update tableName
/// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
Query.whereByField field "@field" |> update tableName
whereByField field "@field" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
@@ -67,7 +126,7 @@ module Query =
/// Query to remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName parameters =
Query.whereById "@id" |> update tableName parameters
whereById "@id" |> update tableName parameters
/// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) =
@@ -76,12 +135,25 @@ module Query =
/// 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
whereByField field "@field" |> update tableName parameters
/// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters)
/// Queries to delete documents
module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Parameter handling helpers
[<AutoOpen>]
@@ -100,12 +172,26 @@ module Parameters =
/// Create a JSON field parameter (name "@field")
[<CompiledName "FSharpAddField">]
let addFieldParam name field parameters =
match field.Op with EX | NEX -> parameters | _ -> SqliteParameter(name, field.Value) :: parameters
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
SqliteParameter($"{name}min", values[0]) :: SqliteParameter($"{name}max", values[1]) :: parameters
| _ -> SqliteParameter(name, field.Value) :: parameters
/// Create a JSON field parameter (name "@field")
let AddField(name, field, parameters) =
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
// let min = SqliteParameter($"{name}min", SqliteType.Integer)
// min.Value <- values[0]
// let max = SqliteParameter($"{name}max", SqliteType.Integer)
// max.Value <- values[1]
[ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ]
// [min; max]
|> Seq.append parameters
| _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
/// Append JSON field name parameters for the given field names to the given parameters

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

@@ -12,7 +12,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,6 @@
using Expecto.CSharp;
using Expecto;
using Microsoft.FSharp.Collections;
namespace BitBadger.Documents.Tests.CSharp;
@@ -96,6 +97,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");
@@ -149,6 +154,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(), new object[] { 18, 49 }, "Value incorrect");
}),
TestCase("EX succeeds", () =>
{
var field = Field.EX("Groovy");
@@ -169,23 +181,6 @@ public static class CommonCSharpTests
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");
})
}),
TestList("Definition", new[]
{
TestCase("EnsureTableFor succeeds", () =>
@@ -226,69 +221,8 @@ 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("Update succeeds", () =>
{
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id",
"UPDATE full statement not correct");
}),
TestList("Count", new[]
{
TestCase("All succeeds", () =>
{
Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Count.ByField("tbl", Field.EQ("thatField", 0)),
"SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field",
"JSON field text comparison count query not correct");
})
}),
TestList("Exists", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Query.Exists.ById("tbl"),
"SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it",
"ID existence query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Query.Exists.ByField("tbl", 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

@@ -47,42 +47,48 @@ public static class PostgresCSharpTests
{
var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>());
Expect.isEmpty(it, "There should not have been any parameters added");
}),
TestCase("succeeds when two parameters are added", () =>
{
var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"),
Enumerable.Empty<Tuple<string, SqlValue>>()).ToList();
Expect.hasLength(it, 2, "There should have been 2 parameters added");
Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct");
Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect");
Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct");
Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect");
})
}),
TestList("RemoveFields", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ById("tbl"),
"UPDATE tbl SET data = data - @name WHERE data ->> 'Id' = @id",
"Remove field by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.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");
}),
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ByContains("tbl"),
"UPDATE tbl SET data = data - @name WHERE data @> @criteria",
"Remove field by contains query not correct");
}),
TestCase("ByJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ByJsonPath("tbl"),
"UPDATE tbl SET data = data - @name WHERE data @? @path::jsonpath",
"Remove field by JSON path query not correct");
})
}),
TestCase("None succeeds", () =>
{
Expect.isEmpty(Parameters.None, "The no-params sequence should be empty");
})
}),
TestList("Query", new[]
{
TestList("WhereByField", new[]
{
TestCase("succeeds when a logical operator is passed", () =>
{
Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds when an existence operator is passed", () =>
{
Expect.equal(Postgres.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL",
"WHERE clause not correct");
}),
TestCase("succeeds when a between operator is passed with numeric values", () =>
{
Expect.equal(Postgres.Query.WhereByField(Field.BT("aField", 50, 99), "@range"),
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
}),
TestCase("succeeds when a between operator is passed with non-numeric values", () =>
{
Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"),
"data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct");
})
}),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct");
}),
TestList("Definition", new[]
{
TestCase("EnsureTable succeeds", () =>
@@ -107,6 +113,11 @@ public static class PostgresCSharpTests
"CREATE INDEX statement not constructed correctly");
})
}),
TestCase("Update succeeds", () =>
{
Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
"UPDATE full statement not correct");
}),
TestCase("WhereDataContains succeeds", () =>
{
Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test",
@@ -119,6 +130,17 @@ public static class PostgresCSharpTests
}),
TestList("Count", new[]
{
TestCase("All succeeds", () =>
{
Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName),
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.Count.ByField(PostgresDb.TableName, Field.EQ("thatField", 0)),
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field",
"JSON field text comparison count query not correct");
}),
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName),
@@ -134,6 +156,18 @@ public static class PostgresCSharpTests
}),
TestList("Exists", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName),
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it",
"ID existence query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ByField(PostgresDb.TableName, Field.LT("Test", 0)),
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it",
"JSON field text comparison exists query not correct");
}),
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName),
@@ -149,6 +183,18 @@ public static class PostgresCSharpTests
}),
TestList("Find", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName),
$"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id",
"SELECT by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)),
$"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field",
"SELECT by JSON comparison query not correct");
}),
TestCase("byContains succeeds", () =>
{
Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName),
@@ -189,8 +235,47 @@ public static class PostgresCSharpTests
"UPDATE partial by JSON Path statement not correct");
})
}),
TestList("RemoveFields", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id",
"Remove field by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ByField(PostgresDb.TableName, Field.LT("Fly", 0)),
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field",
"Remove field by field query not correct");
}),
TestCase("ByContains succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ByContains(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @> @criteria",
"Remove field by contains query not correct");
}),
TestCase("ByJsonPath succeeds", () =>
{
Expect.equal(Postgres.Query.RemoveFields.ByJsonPath(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath",
"Remove field by JSON path query not correct");
})
}),
TestList("Delete", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName),
$"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id",
"DELETE by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ByField(PostgresDb.TableName, Field.NEX("gone")),
$"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL",
"DELETE by JSON comparison query not correct");
}),
TestCase("byContains succeeds", () =>
{
Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName),
@@ -464,13 +549,21 @@ public static class PostgresCSharpTests
var theCount = await Count.All(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents");
}),
TestCase("ByField succeeds", async () =>
TestCase("ByField succeeds for numeric range", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(PostgresDb.TableName, Field.EQ("Value", "purple"));
Expect.equal(theCount, 2, "There should have been 2 matching documents");
var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 3, "There should have been 3 matching documents");
}),
TestCase("ByField succeeds for non-numeric range", async () =>
{
await using var db = PostgresDb.BuildDb();
await LoadDocs();
var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("Value", "aardvark", "apple"));
Expect.equal(theCount, 1, "There should have been 1 matching document");
}),
TestCase("ByContains succeeds", async () =>
{

View File

@@ -53,65 +53,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>

View File

@@ -21,11 +21,81 @@ public static class SqliteCSharpTests
{
TestList("Query", new[]
{
TestList("WhereByField", new[]
{
TestCase("succeeds when a logical operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"),
"data->>'theField' > @test", "WHERE clause not correct");
}),
TestCase("succeeds when an existence operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL",
"WHERE clause not correct");
}),
TestCase("succeeds when the between operator is passed", () =>
{
Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"),
"data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct");
})
}),
TestCase("WhereById succeeds", () =>
{
Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause 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");
}),
TestCase("Update succeeds", () =>
{
Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id",
"UPDATE full statement not correct");
}),
TestList("Count", new[]
{
TestCase("All succeeds", () =>
{
Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl",
"Count query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Sqlite.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", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.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(Sqlite.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(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id",
"SELECT by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Sqlite.Query.Find.ByField("tbl", Field.GE("Golf", 0)),
"SELECT data FROM tbl WHERE data->>'Golf' >= @field",
"SELECT by JSON comparison query not correct");
})
}),
TestList("Patch", new[]
{
TestCase("ById succeeds", () =>
@@ -56,6 +126,19 @@ public static class SqliteCSharpTests
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field",
"Remove field by field query not correct");
})
}),
TestList("Delete", new[]
{
TestCase("ById succeeds", () =>
{
Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id",
"DELETE by ID query not correct");
}),
TestCase("ByField succeeds", () =>
{
Expect.equal(Sqlite.Query.Delete.ByField("tbl", Field.NEX("gone")),
"DELETE FROM tbl WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct");
})
})
}),
TestList("Parameters", new[]
@@ -316,13 +399,21 @@ 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 () =>
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");
}),
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");
})
}),
TestList("Exists", new[]

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,64 @@ let all =
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct"""
}
]
testList "Field" [
test "EQ succeeds" {
let field = Field.EQ "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Op EQ "Operator incorrect"
Expect.equal field.Value 14 "Value incorrect"
}
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"
}
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"
}
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"
}
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"
}
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"
}
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"
}
test "EX succeeds" {
let field = Field.EX "Groovy"
Expect.equal field.Name "Groovy" "Field name incorrect"
Expect.equal field.Op EX "Operator incorrect"
}
test "NEX succeeds" {
let field = Field.NEX "Rad"
Expect.equal field.Name "Rad" "Field name incorrect"
Expect.equal field.Op NEX "Operator incorrect"
}
]
testList "Query" [
test "selectFromTable succeeds" {
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct"
}
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct"
}
testList "whereByField" [
test "succeeds when a logical operator is passed" {
Expect.equal
(Query.whereByField (Field.GT "theField" 0) "@test")
"data ->> 'theField' > @test"
"WHERE clause not correct"
}
test "succeeds when an existence operator is passed" {
Expect.equal
(Query.whereByField (Field.NEX "thatField") "")
"data ->> 'thatField' IS NULL"
"WHERE clause not correct"
}
]
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
@@ -95,65 +135,6 @@ 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 "update succeeds" {
Expect.equal
(Query.update tbl)
$"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id"
"UPDATE full statement not correct"
}
testList "Count" [
test "all succeeds" {
Expect.equal (Query.Count.all tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Count.byField tbl (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"
}
]
]
]

View File

@@ -34,12 +34,59 @@ let unitTests =
let paramList = addFieldParam "@field" (Field.EX "tacos") []
Expect.isEmpty paramList "There should not have been any parameters added"
}
test "succeeds when two parameters are added" {
let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") []
Expect.hasLength paramList 2 "There should have been 2 parameters added"
let min = paramList[0]
Expect.equal (fst min) "@fieldmin" "Minimum field name not correct"
match snd min with
| SqlValue.Parameter value ->
Expect.equal value.ParameterName "@fieldmin" "Minimum parameter name not correct"
Expect.equal value.Value "eh" "Minimum parameter value not correct"
| _ -> Expect.isTrue false "Minimum parameter was not a Parameter type"
let max = paramList[1]
Expect.equal (fst max) "@fieldmax" "Maximum field name not correct"
match snd max with
| SqlValue.Parameter value ->
Expect.equal value.ParameterName "@fieldmax" "Maximum parameter name not correct"
Expect.equal value.Value "zed" "Maximum parameter value not correct"
| _ -> Expect.isTrue false "Maximum parameter was not a Parameter type"
}
]
test "noParams succeeds" {
Expect.isEmpty noParams "The no-params sequence should be empty"
}
]
testList "Query" [
testList "whereByField" [
test "succeeds when a logical operator is passed" {
Expect.equal
(Query.whereByField (Field.GT "theField" 0) "@test")
"data->>'theField' > @test"
"WHERE clause not correct"
}
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 when a between operator is passed with numeric values" {
Expect.equal
(Query.whereByField (Field.BT "aField" 50 99) "@range")
"(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
test "succeeds when a between operator is passed with non-numeric values" {
Expect.equal
(Query.whereByField (Field.BT "field0" "a" "b") "@alpha")
"data->>'field0' BETWEEN @alphamin AND @alphamax"
"WHERE clause not correct"
}
]
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct"
}
testList "Definition" [
test "ensureTable succeeds" {
Expect.equal
@@ -61,6 +108,12 @@ let unitTests =
"CREATE INDEX statement not constructed correctly"
}
]
test "update succeeds" {
Expect.equal
(Query.update PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = @data WHERE data->>'Id' = @id"
"UPDATE full statement not correct"
}
test "whereDataContains succeeds" {
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct"
}
@@ -68,6 +121,18 @@ let unitTests =
Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
}
testList "Count" [
test "all succeeds" {
Expect.equal
(Query.Count.all PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}"
"Count query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0))
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field"
"JSON field text comparison count query not correct"
}
test "byContains succeeds" {
Expect.equal
(Query.Count.byContains PostgresDb.TableName)
@@ -82,6 +147,18 @@ let unitTests =
}
]
testList "Exists" [
test "byId succeeds" {
Expect.equal
(Query.Exists.byId PostgresDb.TableName)
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it"
"ID existence query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Exists.byField PostgresDb.TableName (Field.LT "Test" 0))
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it"
"JSON field text comparison exists query not correct"
}
test "byContains succeeds" {
Expect.equal
(Query.Exists.byContains PostgresDb.TableName)
@@ -96,6 +173,18 @@ let unitTests =
}
]
testList "Find" [
test "byId succeeds" {
Expect.equal
(Query.Find.byId PostgresDb.TableName)
$"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id"
"SELECT by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0))
$"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field"
"SELECT by JSON comparison query not correct"
}
test "byContains succeeds" {
Expect.equal
(Query.Find.byContains PostgresDb.TableName)
@@ -162,6 +251,18 @@ let unitTests =
}
]
testList "Delete" [
test "byId succeeds" {
Expect.equal
(Query.Delete.byId PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id"
"DELETE by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Delete.byField PostgresDb.TableName (Field.NEX "gone"))
$"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL"
"DELETE by JSON comparison query not correct"
}
test "byContains succeeds" {
Expect.equal (Query.Delete.byContains PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria"
@@ -387,12 +488,19 @@ let integrationTests =
let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents"
}
testTask "byField succeeds" {
testTask "byField succeeds for numeric range" {
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.byField PostgresDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 3 "There should have been 3 matching documents"
}
testTask "byField 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()

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

@@ -12,12 +12,80 @@ open Types
let unitTests =
testList "Unit" [
testList "Query" [
testList "whereByField" [
test "succeeds when a logical operator is passed" {
Expect.equal
(Query.whereByField (Field.GT "theField" 0) "@test")
"data->>'theField' > @test"
"WHERE clause not correct"
}
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 when the between operator is passed" {
Expect.equal
(Query.whereByField (Field.BT "aField" 50 99) "@range")
"data->>'aField' BETWEEN @rangemin AND @rangemax"
"WHERE clause not correct"
}
]
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause 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"
}
test "update succeeds" {
Expect.equal
(Query.update "tbl")
"UPDATE tbl SET data = @data WHERE data->>'Id' = @id"
"UPDATE full statement not correct"
}
testList "Count" [
test "all succeeds" {
Expect.equal (Query.Count.all "tbl") $"SELECT COUNT(*) AS it FROM tbl" "Count query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Count.byField "tbl" (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 "Patch" [
test "byId succeeds" {
Expect.equal
@@ -49,6 +117,20 @@ let unitTests =
"Remove field by field 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"
}
]
]
testList "Parameters" [
test "idParam succeeds" {
@@ -299,12 +381,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