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

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <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> <PackageTags>JSON Document SQL</PackageTags>
</PropertyGroup> </PropertyGroup>
@@ -12,7 +13,8 @@
</ItemGroup> </ItemGroup>
<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> </ItemGroup>
</Project> </Project>

View File

@@ -15,6 +15,8 @@ type Op =
| LE | LE
/// Not Equal to (<>) /// Not Equal to (<>)
| NE | NE
/// Between (BETWEEN)
| BT
/// Exists (IS NOT NULL) /// Exists (IS NOT NULL)
| EX | EX
/// Does Not Exist (IS NULL) /// Does Not Exist (IS NULL)
@@ -28,6 +30,7 @@ type Op =
| LT -> "<" | LT -> "<"
| LE -> "<=" | LE -> "<="
| NE -> "<>" | NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL" | EX -> "IS NOT NULL"
| NEX -> "IS NULL" | NEX -> "IS NULL"
@@ -68,11 +71,15 @@ type Field = {
static member NE name (value: obj) = static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value } { 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 /// Create an exists (IS NOT NULL) field criterion
static member EX name = static member EX name =
{ Name = name; Op = EX; Value = obj () } { 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 = static member NEX name =
{ Name = name; Op = NEX; Value = obj () } { Name = name; Op = NEX; Value = obj () }
@@ -150,17 +157,6 @@ module Query =
let selectFromTable tableName = let selectFromTable tableName =
$"SELECT data FROM %s{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 /// Queries to define tables and indexes
module Definition = module Definition =
@@ -202,62 +198,6 @@ module Query =
[<CompiledName "Save">] [<CompiledName "Save">]
let save tableName = let save tableName =
sprintf sprintf
"INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ()) tableName (Configuration.idField ())
/// Query to 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> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.1.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.1.0.0</FileVersion>
<VersionPrefix>3.0.0</VersionPrefix> <VersionPrefix>3.1.0</VersionPrefix>
<VersionSuffix>rc-2</VersionSuffix> <PackageReleaseNotes>Add BT (between) operator; drop .NET 7 support</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://bitbadger.solutions/open-source/relational-documents/</PackageProjectUrl> <PackageProjectUrl>https://bitbadger.solutions/open-source/relational-documents/</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <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> <RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright> <Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <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> <PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
</PropertyGroup> </PropertyGroup>
@@ -14,6 +15,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" /> <PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -63,17 +63,29 @@ module Parameters =
let jsonParam (name: string) (it: 'TJson) = let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it) name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field") /// Create a JSON field parameter
[<CompiledName "FSharpAddField">] [<CompiledName "FSharpAddField">]
let addFieldParam name field parameters = let addFieldParam name field parameters =
match field.Op with match field.Op with
| EX | NEX -> parameters | 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 | _ -> (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 = let AddField name field parameters =
match field.Op with match field.Op with
| EX | NEX -> parameters | 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 | _ -> (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 /// Append JSON field name parameters for the given field names to the given parameters
@@ -97,6 +109,25 @@ module Parameters =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = 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 /// Table and index definition queries
module Definition = module Definition =
@@ -112,6 +143,11 @@ module Query =
let tableName = name.Split '.' |> Array.last let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" $"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 /// Create a WHERE clause fragment to implement a @> (JSON contains) condition
[<CompiledName "WhereDataContains">] [<CompiledName "WhereDataContains">]
let whereDataContains paramName = let whereDataContains paramName =
@@ -125,6 +161,16 @@ module Query =
/// Queries for counting documents /// Queries for counting documents
module Count = 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 (@>) /// Query to count matching documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@@ -138,6 +184,16 @@ module Query =
/// Queries for determining document existence /// Queries for determining document existence
module Exists = 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 (@>) /// Query to determine if documents exist using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@@ -151,6 +207,16 @@ module Query =
/// Queries for retrieving documents /// Queries for retrieving documents
module Find = 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 (@>) /// Query to retrieve documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =
@@ -171,12 +237,12 @@ module Query =
/// Query to patch a document by its ID /// Query to patch a document by its ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName = let byId tableName =
Query.whereById "@id" |> update tableName whereById "@id" |> update tableName
/// Query to patch documents match a JSON field comparison (->> =) /// Query to patch documents match a JSON field comparison (->> =)
[<CompiledName "ByField">] [<CompiledName "ByField">]
let byField tableName field = let byField tableName field =
Query.whereByField field "@field" |> update tableName whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>) /// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
@@ -198,12 +264,12 @@ module Query =
/// Query to remove fields from a document by the document's ID /// Query to remove fields from a document by the document's ID
[<CompiledName "ById">] [<CompiledName "ById">]
let byId tableName = 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 /// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "ByField">] [<CompiledName "ByField">]
let byField tableName field = let byField tableName field =
Query.whereByField field "@field" |> update tableName whereByField field "@field" |> update tableName
/// Query to patch documents matching a JSON containment query (@>) /// Query to patch documents matching a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
@@ -218,6 +284,16 @@ module Query =
/// Queries to delete documents /// Queries to delete documents
module Delete = 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 (@>) /// Query to delete documents using a JSON containment query (@>)
[<CompiledName "ByContains">] [<CompiledName "ByContains">]
let byContains tableName = let byContains tableName =

View File

@@ -45,7 +45,7 @@ Retrieve all customers:
```csharp ```csharp
// C#; parameter is table name // C#; parameter is table name
// Find.All type signature is Func<string, Task<List<TDoc>>> // Find.All type signature is Func<string, Task<List<TDoc>>>
var customers = await Find.All("customer"); var customers = await Find.All<Customer>("customer");
``` ```
```fsharp ```fsharp

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <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> <PackageTags>JSON Document SQLite</PackageTags>
</PropertyGroup> </PropertyGroup>
@@ -13,7 +14,8 @@
</ItemGroup> </ItemGroup>
<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>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="10.1.0" /> <PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" /> <PackageReference Include="ThrowawayDb.Postgres" Version="1.4.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,5 +1,6 @@
using Expecto.CSharp; using Expecto.CSharp;
using Expecto; using Expecto;
using Microsoft.FSharp.Collections;
namespace BitBadger.Documents.Tests.CSharp; 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"); 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", () => TestCase("EX succeeds", () =>
{ {
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); 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.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value 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", () => TestCase("EX succeeds", () =>
{ {
var field = Field.EX("Groovy"); var field = Field.EX("Groovy");
@@ -169,23 +181,6 @@ public static class CommonCSharpTests
Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table",
"SELECT statement not correct"); "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[] TestList("Definition", new[]
{ {
TestCase("EnsureTableFor succeeds", () => TestCase("EnsureTableFor succeeds", () =>
@@ -226,69 +221,8 @@ public static class CommonCSharpTests
TestCase("Save succeeds", () => TestCase("Save succeeds", () =>
{ {
Expect.equal(Query.Save("tbl"), 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"); "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>>()); var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty<Tuple<string, SqlValue>>());
Expect.isEmpty(it, "There should not have been any parameters added"); 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("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[] TestList("Definition", new[]
{ {
TestCase("EnsureTable succeeds", () => TestCase("EnsureTable succeeds", () =>
@@ -107,6 +113,11 @@ public static class PostgresCSharpTests
"CREATE INDEX statement not constructed correctly"); "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", () => TestCase("WhereDataContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test",
@@ -119,6 +130,17 @@ public static class PostgresCSharpTests
}), }),
TestList("Count", new[] 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", () => TestCase("ByContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName),
@@ -134,6 +156,18 @@ public static class PostgresCSharpTests
}), }),
TestList("Exists", new[] 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", () => TestCase("ByContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName),
@@ -149,6 +183,18 @@ public static class PostgresCSharpTests
}), }),
TestList("Find", new[] 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", () => TestCase("byContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName),
@@ -167,13 +213,13 @@ public static class PostgresCSharpTests
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id",
"UPDATE partial by ID statement not correct"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)),
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field",
"UPDATE partial by ID statement not correct"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByContains succeeds", () => TestCase("ByContains succeeds", () =>
@@ -189,8 +235,47 @@ public static class PostgresCSharpTests
"UPDATE partial by JSON Path statement not correct"); "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[] 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", () => TestCase("byContains succeeds", () =>
{ {
Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName),
@@ -464,13 +549,21 @@ public static class PostgresCSharpTests
var theCount = await Count.All(PostgresDb.TableName); var theCount = await Count.All(PostgresDb.TableName);
Expect.equal(theCount, 5, "There should have been 5 matching documents"); Expect.equal(theCount, 5, "There should have been 5 matching documents");
}), }),
TestCase("ByField succeeds", async () => TestCase("ByField succeeds for numeric range", async () =>
{ {
await using var db = PostgresDb.BuildDb(); await using var db = PostgresDb.BuildDb();
await LoadDocs(); await LoadDocs();
var theCount = await Count.ByField(PostgresDb.TableName, Field.EQ("Value", "purple")); var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 2, "There should have been 2 matching documents"); 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 () => TestCase("ByContains succeeds", async () =>
{ {

View File

@@ -53,65 +53,55 @@ public static class PostgresDb
/// The host for the database /// The host for the database
/// </summary> /// </summary>
private static readonly Lazy<string> DbHost = new(() => private static readonly Lazy<string> DbHost = new(() =>
{ Environment.GetEnvironmentVariable("BBDOX_PG_HOST") switch
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbHost") switch
{ {
null => "localhost", null => "localhost",
var host when host.Trim() == "" => "localhost", var host when host.Trim() == "" => "localhost",
var host => host var host => host
};
}); });
/// <summary> /// <summary>
/// The port for the database /// The port for the database
/// </summary> /// </summary>
private static readonly Lazy<int> DbPort = new(() => private static readonly Lazy<int> DbPort = new(() =>
{ Environment.GetEnvironmentVariable("BBDOX_PG_PORT") switch
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbPort") switch
{ {
null => 5432, null => 5432,
var port when port.Trim() == "" => 5432, var port when port.Trim() == "" => 5432,
var port => int.Parse(port) var port => int.Parse(port)
};
}); });
/// <summary> /// <summary>
/// The database itself /// The database itself
/// </summary> /// </summary>
private static readonly Lazy<string> DbDatabase = new(() => private static readonly Lazy<string> DbDatabase = new(() =>
{ Environment.GetEnvironmentVariable("BBDOX_PG_DATABASE") switch
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postres.DbDatabase") switch
{ {
null => "postgres", null => "postgres",
var db when db.Trim() == "" => "postgres", var db when db.Trim() == "" => "postgres",
var db => db var db => db
};
}); });
/// <summary> /// <summary>
/// The user to use in connecting to the database /// The user to use in connecting to the database
/// </summary> /// </summary>
private static readonly Lazy<string> DbUser = new(() => private static readonly Lazy<string> DbUser = new(() =>
{ Environment.GetEnvironmentVariable("BBDOX_PG_USER") switch
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbUser") switch
{ {
null => "postgres", null => "postgres",
var user when user.Trim() == "" => "postgres", var user when user.Trim() == "" => "postgres",
var user => user var user => user
};
}); });
/// <summary> /// <summary>
/// The password to use for the database /// The password to use for the database
/// </summary> /// </summary>
private static readonly Lazy<string> DbPassword = new(() => private static readonly Lazy<string> DbPassword = new(() =>
{ Environment.GetEnvironmentVariable("BBDOX_PG_PWD") switch
return Environment.GetEnvironmentVariable("BitBadger.Documents.Postrgres.DbPwd") switch
{ {
null => "postgres", null => "postgres",
var pwd when pwd.Trim() == "" => "postgres", var pwd when pwd.Trim() == "" => "postgres",
var pwd => pwd var pwd => pwd
};
}); });
/// <summary> /// <summary>

View File

@@ -21,23 +21,93 @@ public static class SqliteCSharpTests
{ {
TestList("Query", new[] 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", () => TestCase("Definition.EnsureTable succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"),
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); "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[] TestList("Patch", new[]
{ {
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Patch.ById("tbl"), Expect.equal(Sqlite.Query.Patch.ById("tbl"),
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id",
"UPDATE partial by ID statement not correct"); "UPDATE partial by ID statement not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Sqlite.Query.Patch.ByField("tbl", Field.NE("Part", 0)), 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 tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field",
"UPDATE partial by JSON comparison query not correct"); "UPDATE partial by JSON comparison query not correct");
}) })
}), }),
@@ -46,16 +116,29 @@ public static class SqliteCSharpTests
TestCase("ById succeeds", () => TestCase("ById succeeds", () =>
{ {
Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }), Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }),
"UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id", "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id",
"Remove field by ID query not correct"); "Remove field by ID query not correct");
}), }),
TestCase("ByField succeeds", () => TestCase("ByField succeeds", () =>
{ {
Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0), Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0),
new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }), new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }),
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data ->> 'Fly' < @field", "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field",
"Remove field by field query not correct"); "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[] TestList("Parameters", new[]
@@ -316,13 +399,21 @@ public static class SqliteCSharpTests
var theCount = await Count.All(SqliteDb.TableName); var theCount = await Count.All(SqliteDb.TableName);
Expect.equal(theCount, 5L, "There should have been 5 matching documents"); 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 using var db = await SqliteDb.BuildDb();
await LoadDocs(); await LoadDocs();
var theCount = await Count.ByField(SqliteDb.TableName, Field.EQ("Value", "purple")); var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("NumValue", 10, 20));
Expect.equal(theCount, 2L, "There should have been 2 matching documents"); 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[] TestList("Exists", new[]

View File

@@ -15,7 +15,8 @@
</ItemGroup> </ItemGroup>
<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>
<ItemGroup> <ItemGroup>

View File

@@ -28,6 +28,9 @@ let all =
test "NE succeeds" { test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct" 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" { test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" 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""" 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" [ testList "Query" [
test "selectFromTable succeeds" { test "selectFromTable succeeds" {
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" 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" [ testList "Definition" [
test "ensureTableFor succeeds" { test "ensureTableFor succeeds" {
Expect.equal Expect.equal
@@ -92,68 +132,9 @@ let all =
test "save succeeds" { test "save succeeds" {
Expect.equal Expect.equal
(Query.save tbl) (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" "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") [] let paramList = addFieldParam "@field" (Field.EX "tacos") []
Expect.isEmpty paramList "There should not have been any parameters added" 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" { test "noParams succeeds" {
Expect.isEmpty noParams "The no-params sequence should be empty" Expect.isEmpty noParams "The no-params sequence should be empty"
} }
] ]
testList "Query" [ 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" [ testList "Definition" [
test "ensureTable succeeds" { test "ensureTable succeeds" {
Expect.equal Expect.equal
@@ -61,6 +108,12 @@ let unitTests =
"CREATE INDEX statement not constructed correctly" "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" { test "whereDataContains succeeds" {
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" 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" Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
} }
testList "Count" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Count.byContains PostgresDb.TableName) (Query.Count.byContains PostgresDb.TableName)
@@ -82,6 +147,18 @@ let unitTests =
} }
] ]
testList "Exists" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Exists.byContains PostgresDb.TableName) (Query.Exists.byContains PostgresDb.TableName)
@@ -96,6 +173,18 @@ let unitTests =
} }
] ]
testList "Find" [ 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" { test "byContains succeeds" {
Expect.equal Expect.equal
(Query.Find.byContains PostgresDb.TableName) (Query.Find.byContains PostgresDb.TableName)
@@ -113,13 +202,13 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.Patch.byId PostgresDb.TableName) (Query.Patch.byId PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id"
"UPDATE partial by ID statement not correct" "UPDATE partial by ID statement not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) (Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0))
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field" $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field"
"UPDATE partial by ID statement not correct" "UPDATE partial by ID statement not correct"
} }
test "byContains succeeds" { test "byContains succeeds" {
@@ -139,13 +228,13 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byId "tbl") (Query.RemoveFields.byId "tbl")
"UPDATE tbl SET data = data - @name WHERE data ->> 'Id' = @id" "UPDATE tbl SET data = data - @name WHERE data->>'Id' = @id"
"Remove field by ID query not correct" "Remove field by ID query not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0)) (Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0))
"UPDATE tbl SET data = data - @name WHERE data ->> 'Fly' < @field" "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field"
"Remove field by field query not correct" "Remove field by field query not correct"
} }
test "byContains succeeds" { test "byContains succeeds" {
@@ -162,6 +251,18 @@ let unitTests =
} }
] ]
testList "Delete" [ 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" { test "byContains succeeds" {
Expect.equal (Query.Delete.byContains PostgresDb.TableName) Expect.equal (Query.Delete.byContains PostgresDb.TableName)
$"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria"
@@ -387,12 +488,19 @@ let integrationTests =
let! theCount = Count.all PostgresDb.TableName let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents" Expect.equal theCount 5 "There should have been 5 matching documents"
} }
testTask "byField succeeds" { testTask "byField succeeds for numeric range" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
do! loadDocs () do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName (Field.EQ "Value" "purple") let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 2 "There should have been 2 matching documents" 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" { testTask "byContains succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()

View File

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

View File

@@ -12,23 +12,91 @@ open Types
let unitTests = let unitTests =
testList "Unit" [ testList "Unit" [
testList "Query" [ 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" { test "Definition.ensureTable succeeds" {
Expect.equal Expect.equal
(Query.Definition.ensureTable "tbl") (Query.Definition.ensureTable "tbl")
"CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)"
"CREATE TABLE statement not correct" "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" [ testList "Patch" [
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.Patch.byId "tbl") (Query.Patch.byId "tbl")
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id"
"UPDATE partial by ID statement not correct" "UPDATE partial by ID statement not correct"
} }
test "byField succeeds" { test "byField succeeds" {
Expect.equal Expect.equal
(Query.Patch.byField "tbl" (Field.NE "Part" 0)) (Query.Patch.byField "tbl" (Field.NE "Part" 0))
"UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field"
"UPDATE partial by JSON comparison query not correct" "UPDATE partial by JSON comparison query not correct"
} }
] ]
@@ -36,7 +104,7 @@ let unitTests =
test "byId succeeds" { test "byId succeeds" {
Expect.equal Expect.equal
(Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ]) (Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ])
"UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id" "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id"
"Remove field by ID query not correct" "Remove field by ID query not correct"
} }
test "byField succeeds" { test "byField succeeds" {
@@ -45,10 +113,24 @@ let unitTests =
"tbl" "tbl"
(Field.GT "Fly" 0) (Field.GT "Fly" 0)
[ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ]) [ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ])
"UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data ->> 'Fly' > @field" "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' > @field"
"Remove field by field query not correct" "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" [ testList "Parameters" [
test "idParam succeeds" { test "idParam succeeds" {
@@ -299,12 +381,19 @@ let integrationTests =
let! theCount = Count.all SqliteDb.TableName let! theCount = Count.all SqliteDb.TableName
Expect.equal theCount 5L "There should have been 5 matching documents" Expect.equal theCount 5L "There should have been 5 matching documents"
} }
testTask "byField succeeds" { testTask "byField succeeds for a numeric range" {
use! db = SqliteDb.BuildDb() use! db = SqliteDb.BuildDb()
do! loadDocs () do! loadDocs ()
let! theCount = Count.byField SqliteDb.TableName (Field.EQ "Value" "purple") let! theCount = Count.byField SqliteDb.TableName (Field.BT "NumValue" 10 20)
Expect.equal theCount 2L "There should have been 2 matching documents" 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" [ 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