2 Commits
v4-rc4 ... v4

Author SHA1 Message Date
147a72b476 Final tweaks for v4 (#9)
- Add .NET 9, PostgreSQL 17 support
- Drop .NET 6, PostgreSQL 12 support
- Finalize READMEs

Reviewed-on: #9
2024-12-18 03:33:11 +00:00
740767661c Make Field constructor functions generic (#8)
F# can upcast types to `obj` if those types are used in place. However, a `string seq` (`IEnumerable<string>` in C#) cannot be upcast to an `obj seq` (`IEnumerable<object>`) without mapping each item in the sequence. Making the `Field` constructor functions generic will allow them to take any object type, and these functions handle the conversion to `obj` (for `In` and `InArray`; others work transparently).

Reviewed-on: #8
2024-09-18 13:36:14 +00:00
17 changed files with 158 additions and 112 deletions

View File

@@ -13,7 +13,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" /> <PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="System.Text.Encodings.Web" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,26 +4,37 @@ open System.Security.Cryptography
/// The types of comparisons available for JSON fields /// The types of comparisons available for JSON fields
type Comparison = type Comparison =
/// Equals (=) /// Equals (=)
| Equal of Value: obj | Equal of Value: obj
/// Greater Than (>) /// Greater Than (>)
| Greater of Value: obj | Greater of Value: obj
/// Greater Than or Equal To (>=) /// Greater Than or Equal To (>=)
| GreaterOrEqual of Value: obj | GreaterOrEqual of Value: obj
/// Less Than (<) /// Less Than (<)
| Less of Value: obj | Less of Value: obj
/// Less Than or Equal To (<=) /// Less Than or Equal To (<=)
| LessOrEqual of Value: obj | LessOrEqual of Value: obj
/// Not Equal to (<>) /// Not Equal to (<>)
| NotEqual of Value: obj | NotEqual of Value: obj
/// Between (BETWEEN) /// Between (BETWEEN)
| Between of Min: obj * Max: obj | Between of Min: obj * Max: obj
/// In (IN) /// In (IN)
| In of Values: obj seq | In of Values: obj seq
/// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN) /// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
| InArray of Table: string * Values: obj seq | InArray of Table: string * Values: obj seq
/// Exists (IS NOT NULL) /// Exists (IS NOT NULL)
| Exists | Exists
/// Does Not Exist (IS NULL) /// Does Not Exist (IS NULL)
| NotExists | NotExists
@@ -53,15 +64,18 @@ type Dialect =
/// The format in which an element of a JSON field should be extracted /// The format in which an element of a JSON field should be extracted
[<Struct>] [<Struct>]
type FieldFormat = type FieldFormat =
/// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value /// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
| AsSql | AsSql
/// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value /// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
| AsJson | AsJson
/// Criteria for a field WHERE clause /// Criteria for a field WHERE clause
type Field = type Field = {
{ /// The name of the field
/// The name of the field
Name: string Name: string
/// The comparison for the field /// The comparison for the field
@@ -71,72 +85,72 @@ type Field =
ParameterName: string option ParameterName: string option
/// The table qualifier for this field /// The table qualifier for this field
Qualifier: string option } Qualifier: string option
with } with
/// Create a comparison against a field /// Create a comparison against a field
static member Where name comparison = static member Where name (comparison: Comparison) =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None } { Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
/// Create an equals (=) field criterion /// Create an equals (=) field criterion
static member Equal name (value: obj) = static member Equal<'T> name (value: 'T) =
Field.Where name (Equal value) Field.Where name (Equal value)
/// Create an equals (=) field criterion (alias) /// Create an equals (=) field criterion (alias)
static member EQ name (value: obj) = Field.Equal name value static member EQ<'T> name (value: 'T) = Field.Equal name value
/// Create a greater than (>) field criterion /// Create a greater than (>) field criterion
static member Greater name (value: obj) = static member Greater<'T> name (value: 'T) =
Field.Where name (Greater value) Field.Where name (Greater value)
/// Create a greater than (>) field criterion (alias) /// Create a greater than (>) field criterion (alias)
static member GT name (value: obj) = Field.Greater name value static member GT<'T> name (value: 'T) = Field.Greater name value
/// Create a greater than or equal to (>=) field criterion /// Create a greater than or equal to (>=) field criterion
static member GreaterOrEqual name (value: obj) = static member GreaterOrEqual<'T> name (value: 'T) =
Field.Where name (GreaterOrEqual value) Field.Where name (GreaterOrEqual value)
/// Create a greater than or equal to (>=) field criterion (alias) /// Create a greater than or equal to (>=) field criterion (alias)
static member GE name (value: obj) = Field.GreaterOrEqual name value static member GE<'T> name (value: 'T) = Field.GreaterOrEqual name value
/// Create a less than (<) field criterion /// Create a less than (<) field criterion
static member Less name (value: obj) = static member Less<'T> name (value: 'T) =
Field.Where name (Less value) Field.Where name (Less value)
/// Create a less than (<) field criterion (alias) /// Create a less than (<) field criterion (alias)
static member LT name (value: obj) = Field.Less name value static member LT<'T> name (value: 'T) = Field.Less name value
/// Create a less than or equal to (<=) field criterion /// Create a less than or equal to (<=) field criterion
static member LessOrEqual name (value: obj) = static member LessOrEqual<'T> name (value: 'T) =
Field.Where name (LessOrEqual value) Field.Where name (LessOrEqual value)
/// Create a less than or equal to (<=) field criterion (alias) /// Create a less than or equal to (<=) field criterion (alias)
static member LE name (value: obj) = Field.LessOrEqual name value static member LE<'T> name (value: 'T) = Field.LessOrEqual name value
/// Create a not equals (<>) field criterion /// Create a not equals (<>) field criterion
static member NotEqual name (value: obj) = static member NotEqual<'T> name (value: 'T) =
Field.Where name (NotEqual value) Field.Where name (NotEqual value)
/// Create a not equals (<>) field criterion (alias) /// Create a not equals (<>) field criterion (alias)
static member NE name (value: obj) = Field.NotEqual name value static member NE<'T> name (value: 'T) = Field.NotEqual name value
/// Create a Between field criterion /// Create a Between field criterion
static member Between name (min: obj) (max: obj) = static member Between<'T> name (min: 'T) (max: 'T) =
Field.Where name (Between(min, max)) Field.Where name (Between(min, max))
/// Create a Between field criterion (alias) /// Create a Between field criterion (alias)
static member BT name (min: obj) (max: obj) = Field.Between name min max static member BT<'T> name (min: 'T) (max: 'T) = Field.Between name min max
/// Create an In field criterion /// Create an In field criterion
static member In name (values: obj seq) = static member In<'T> name (values: 'T seq) =
Field.Where name (In values) Field.Where name (In (Seq.map box values))
/// Create an In field criterion (alias) /// Create an In field criterion (alias)
static member IN name (values: obj seq) = Field.In name values static member IN<'T> name (values: 'T seq) = Field.In name values
/// Create an InArray field criterion /// Create an InArray field criterion
static member InArray name tableName (values: obj seq) = static member InArray<'T> name tableName (values: 'T seq) =
Field.Where name (InArray(tableName, values)) Field.Where name (InArray(tableName, Seq.map box values))
/// Create an exists (IS NOT NULL) field criterion /// Create an exists (IS NOT NULL) field criterion
static member Exists name = static member Exists name =
@@ -190,8 +204,10 @@ with
/// How fields should be matched /// How fields should be matched
[<Struct>] [<Struct>]
type FieldMatch = type FieldMatch =
/// Any field matches (OR) /// Any field matches (OR)
| Any | Any
/// All fields match (AND) /// All fields match (AND)
| All | All
@@ -202,6 +218,7 @@ type FieldMatch =
/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) /// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)
type ParameterName() = type ParameterName() =
/// The counter for the next field value /// The counter for the next field value
let mutable currentIdx = -1 let mutable currentIdx = -1
@@ -213,19 +230,20 @@ type ParameterName() =
currentIdx <- currentIdx + 1 currentIdx <- currentIdx + 1
$"@field{currentIdx}" $"@field{currentIdx}"
#if NET6_0
open System.Text
#endif
/// Automatically-generated document ID strategies /// Automatically-generated document ID strategies
[<Struct>] [<Struct>]
type AutoId = type AutoId =
/// No automatic IDs will be generated /// No automatic IDs will be generated
| Disabled | Disabled
/// Generate a MAX-plus-1 numeric value for documents /// Generate a MAX-plus-1 numeric value for documents
| Number | Number
/// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string) /// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
| Guid | Guid
/// Generate a random string of hexadecimal characters for each document /// Generate a random string of hexadecimal characters for each document
| RandomString | RandomString
with with
@@ -235,13 +253,7 @@ with
/// Generate a string of random hexadecimal characters /// Generate a string of random hexadecimal characters
static member GenerateRandomString(length: int) = static member GenerateRandomString(length: int) =
#if NET8_0_OR_GREATER
RandomNumberGenerator.GetHexString(length, lowercase = true) RandomNumberGenerator.GetHexString(length, lowercase = true)
#else
RandomNumberGenerator.GetBytes((length / 2) + 1)
|> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length)
|> function it -> it.Length <- length; it.ToString()
#endif
/// Does the given document need an automatic ID generated? /// Does the given document need an automatic ID generated?
static member NeedsAutoId<'T> strategy (document: 'T) idProp = static member NeedsAutoId<'T> strategy (document: 'T) idProp =

View File

@@ -7,6 +7,7 @@ This package provides common definitions and functionality for `BitBadger.Docume
## Features ## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries) - Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries)
- Accesses documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s) - Accesses documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Uses `Task`-based async for all data access functions - Uses `Task`-based async for all data access functions

View File

@@ -1,13 +1,19 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>4.0.0.0</AssemblyVersion> <AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion> <FileVersion>4.0.0.0</FileVersion>
<VersionPrefix>4.0.0</VersionPrefix> <VersionPrefix>4.0.0</VersionPrefix>
<VersionSuffix>rc4</VersionSuffix> <PackageReleaseNotes>From v3.1: (see project site for breaking changes and compatibility)
<PackageReleaseNotes>From rc3: Add In/InArray field comparisons, revamp internal comparison handling. From rc2: preserve additional ORDER BY qualifiers. From rc1: add case-insensitive ordering. From v3.1: Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility</PackageReleaseNotes> - Change ByField to ByFields
- Support dot-access to nested document fields
- Add Find*Ordered functions/methods
- Add case-insensitive ordering (as of rc2)
- Preserve additional ORDER BY qualifiers (as of rc3)
- Add In / InArray comparisons (as of rc4)
- Field construction functions are generic (as of rc5)</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>

View File

@@ -14,8 +14,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" /> <PackageReference Include="Npgsql" Version="9.0.2" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Include="Npgsql.FSharp" Version="8.0.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -71,7 +71,7 @@ module WithProps =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, sqlProps) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@@ -144,7 +144,7 @@ module Find =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
@@ -248,7 +248,7 @@ type NpgsqlConnectionCSharpCompatExtensions =
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>] [<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn) WithProps.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =)

View File

@@ -211,7 +211,7 @@ type NpgsqlConnectionCSharpExtensions =
/// Execute a query that returns one or no results; returns None if not found /// Execute a query that returns one or no results; returns None if not found
[<Extension>] [<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null>( static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) = conn, query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn) WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn)
@@ -303,7 +303,7 @@ type NpgsqlConnectionCSharpExtensions =
/// Retrieve a document by its ID; returns None if not found /// Retrieve a document by its ID; returns None if not found
[<Extension>] [<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, Sql.existingConnection conn) WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =) /// Retrieve documents matching a JSON field comparison query (->> =)
@@ -339,38 +339,41 @@ type NpgsqlConnectionCSharpExtensions =
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>] [<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the
/// document; returns null if not found /// document; returns null if not found
[<Extension>] [<Extension>]
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, queryFields, orderFields) = conn, tableName, howMatched, queryFields, orderFields) =
WithProps.Find.FirstByFieldsOrdered<'TDoc>( WithProps.Find.FirstByFieldsOrdered<'TDoc>(
tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found /// Retrieve the first document matching a JSON containment query (@>); returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = static member inline FindFirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document;
/// returns None if not found /// returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null>( static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, criteria: obj, orderFields) = conn, tableName, criteria: obj, orderFields) =
WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn)
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document;
/// returns None if not found /// returns None if not found
[<Extension>] [<Extension>]
static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null>(conn, tableName, jsonPath, orderFields) = static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, jsonPath, orderFields) =
WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn)
/// Update an entire document by its ID /// Update an entire document by its ID

View File

@@ -3,8 +3,10 @@
/// The type of index to generate for the document /// The type of index to generate for the document
[<Struct>] [<Struct>]
type DocumentIndex = type DocumentIndex =
/// A GIN index with standard operations (all operators supported) /// A GIN index with standard operations (all operators supported)
| Full | Full
/// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators)
| Optimized | Optimized
@@ -36,6 +38,7 @@ open Npgsql.FSharp
/// Helper functions /// Helper functions
[<AutoOpen>] [<AutoOpen>]
module private Helpers = module private Helpers =
/// Shorthand to retrieve the data source as SqlProps /// Shorthand to retrieve the data source as SqlProps
let internal fromDataSource () = let internal fromDataSource () =
Configuration.dataSource () |> Sql.fromDataSource Configuration.dataSource () |> Sql.fromDataSource
@@ -272,7 +275,7 @@ module WithProps =
} }
/// Execute a query that returns one or no results; returns null if not found /// Execute a query that returns one or no results; returns null if not found
let Single<'TDoc when 'TDoc: null>( let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask { query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps
return Option.toObj result return Option.toObj result
@@ -439,7 +442,7 @@ module WithProps =
Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps
/// Retrieve a document by its ID (returns null if not found) /// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps) Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps)
@@ -549,7 +552,7 @@ module WithProps =
sqlProps sqlProps
/// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [], addFieldParams fields [],
@@ -568,7 +571,8 @@ module WithProps =
/// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the
/// document; returns null if not found /// document; returns null if not found
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, sqlProps) = let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1", $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
addFieldParams queryFields [], addFieldParams queryFields [],
@@ -585,7 +589,7 @@ module WithProps =
sqlProps sqlProps
/// Retrieve the first document matching a JSON containment query (@>); returns null if not found /// Retrieve the first document matching a JSON containment query (@>); returns null if not found
let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) = let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byContains (Query.find tableName)} LIMIT 1", $"{Query.byContains (Query.find tableName)} LIMIT 1",
[ jsonParam "@criteria" criteria ], [ jsonParam "@criteria" criteria ],
@@ -604,7 +608,8 @@ module WithProps =
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the
/// document; returns null if not found /// document; returns null if not found
let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields, sqlProps) = let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, criteria: obj, orderFields, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
[ jsonParam "@criteria" criteria ], [ jsonParam "@criteria" criteria ],
@@ -621,7 +626,7 @@ module WithProps =
sqlProps sqlProps
/// Retrieve the first document matching a JSON Path match query (@?); returns null if not found /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found
let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) = let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byPathMatch (Query.find tableName)} LIMIT 1", $"{Query.byPathMatch (Query.find tableName)} LIMIT 1",
[ "@path", Sql.string jsonPath ], [ "@path", Sql.string jsonPath ],
@@ -640,7 +645,8 @@ module WithProps =
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the
/// document; returns null if not found /// document; returns null if not found
let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields, sqlProps) = let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, jsonPath, orderFields, sqlProps) =
Custom.Single<'TDoc>( Custom.Single<'TDoc>(
$"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1",
[ "@path", Sql.string jsonPath ], [ "@path", Sql.string jsonPath ],
@@ -779,7 +785,8 @@ module Custom =
WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ()) WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ())
/// Execute a query that returns one or no results; returns null if not found /// Execute a query that returns one or no results; returns null if not found
let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) = let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ()) WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// Execute a query that returns no results /// Execute a query that returns no results
@@ -910,7 +917,7 @@ module Find =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ()) WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ())
/// Retrieve a document by its ID; returns null if not found /// Retrieve a document by its ID; returns null if not found
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey) = let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ())
/// Retrieve documents matching a JSON field comparison query (->> =) /// Retrieve documents matching a JSON field comparison query (->> =)
@@ -973,7 +980,7 @@ module Find =
WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ()) WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the
@@ -984,7 +991,8 @@ module Find =
/// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the
/// document; returns null if not found /// document; returns null if not found
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields) =
WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ())
/// Retrieve the first document matching a JSON containment query (@>); returns None if not found /// Retrieve the first document matching a JSON containment query (@>); returns None if not found
@@ -993,7 +1001,7 @@ module Find =
WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ()) WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ())
/// Retrieve the first document matching a JSON containment query (@>); returns null if not found /// Retrieve the first document matching a JSON containment query (@>); returns null if not found
let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document;
@@ -1004,7 +1012,7 @@ module Find =
/// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document;
/// returns null if not found /// returns null if not found
let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields) = let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, orderFields) =
WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ())
/// Retrieve the first document matching a JSON Path match query (@?); returns None if not found /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found
@@ -1013,7 +1021,7 @@ module Find =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// Retrieve the first document matching a JSON Path match query (@?); returns null if not found /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found
let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document;
@@ -1024,7 +1032,7 @@ module Find =
/// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document;
/// returns null if not found /// returns null if not found
let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields) = let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, orderFields) =
WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ())

View File

@@ -5,11 +5,16 @@ This package provides a lightweight document library backed by [PostgreSQL](http
## Features ## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries) - Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries)
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s) - Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions - Use `Task`-based async for all data access functions
- Use building blocks for more complex queries - Use building blocks for more complex queries
## Upgrading from v3
There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details.
## Getting Started ## Getting Started
Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library: Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library:

View File

@@ -14,8 +14,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Update="FSharp.Core" Version="9.0.100" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -71,7 +71,7 @@ module WithConn =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, conn) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn) WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@@ -144,7 +144,7 @@ module Find =
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
@@ -247,7 +247,7 @@ type SqliteConnectionCSharpCompatExtensions =
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>] [<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">] [<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn) WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =)

View File

@@ -1,6 +1,5 @@
namespace BitBadger.Documents.Sqlite namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
/// F# extensions for the SqliteConnection type /// F# extensions for the SqliteConnection type
@@ -131,7 +130,7 @@ type SqliteConnectionCSharpExtensions =
/// Execute a query that returns one or no results /// Execute a query that returns one or no results
[<Extension>] [<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null>( static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) = conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
@@ -198,7 +197,7 @@ type SqliteConnectionCSharpExtensions =
/// Retrieve a document by its ID /// Retrieve a document by its ID
[<Extension>] [<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) =
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on JSON fields /// Retrieve documents via a comparison on JSON fields
@@ -213,13 +212,14 @@ type SqliteConnectionCSharpExtensions =
/// Retrieve documents via a comparison on JSON fields, returning only the first result /// Retrieve documents via a comparison on JSON fields, returning only the first result
[<Extension>] [<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, fields) =
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only
/// the first result /// the first result
[<Extension>] [<Extension>]
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, queryFields, orderFields) = conn, tableName, howMatched, queryFields, orderFields) =
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
@@ -262,9 +262,3 @@ type SqliteConnectionCSharpExtensions =
[<Extension>] [<Extension>]
static member inline DeleteByFields(conn, tableName, howMatched, fields) = static member inline DeleteByFields(conn, tableName, howMatched, fields) =
WithConn.Delete.byFields tableName howMatched fields conn WithConn.Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead; will be removed in v4">]
static member inline DeleteByField(conn, tableName, field) =
conn.DeleteByFields(tableName, Any, [ field ])

View File

@@ -153,7 +153,7 @@ module Results =
/// Create a domain item from a document, specifying the field in which the document is found /// Create a domain item from a document, specifying the field in which the document is found
[<CompiledName "FromDocument">] [<CompiledName "FromDocument">]
let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc =
Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal field))
/// Create a domain item from a document /// Create a domain item from a document
[<CompiledName "FromData">] [<CompiledName "FromData">]
@@ -221,7 +221,7 @@ module WithConn =
} }
/// Execute a query that returns one or no results (returns null if not found) /// Execute a query that returns one or no results (returns null if not found)
let Single<'TDoc when 'TDoc: null>( let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn
) = backgroundTask { ) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke conn let! result = single<'TDoc> query parameters mapFunc.Invoke conn
@@ -358,7 +358,7 @@ module WithConn =
Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn
/// Retrieve a document by its ID (returns null if not found) /// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey, conn) =
Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn) Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on JSON fields /// Retrieve documents via a comparison on JSON fields
@@ -405,7 +405,7 @@ module WithConn =
conn conn
/// Retrieve documents via a comparison on JSON fields, returning only the first result /// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, conn) =
Custom.Single( Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [], addFieldParams fields [],
@@ -424,7 +424,8 @@ module WithConn =
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning
/// only the first result /// only the first result
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, conn) = let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields, conn) =
Custom.Single( Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1", $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1",
addFieldParams queryFields [], addFieldParams queryFields [],
@@ -529,7 +530,8 @@ module Custom =
WithConn.Custom.single<'TDoc> query parameters mapFunc conn WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// Execute a query that returns one or no results (returns null if not found) /// Execute a query that returns one or no results (returns null if not found)
let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) = let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
@@ -652,7 +654,7 @@ module Find =
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve a document by its ID (returns null if not found) /// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId) = let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
@@ -685,7 +687,7 @@ module Find =
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on JSON fields, returning only the first result /// Retrieve documents via a comparison on JSON fields, returning only the first result
let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
@@ -698,7 +700,8 @@ module Find =
/// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only
/// the first result /// the first result
let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)

View File

@@ -5,11 +5,16 @@ This package provides a lightweight document library backed by [SQLite](https://
## Features ## Features
- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents
- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings)
- Address documents via ID or via comparison on any field - Address documents via ID or via comparison on any field
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s) - Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s)
- Use `Task`-based async for all data access functions - Use `Task`-based async for all data access functions
- Use building blocks for more complex queries - Use building blocks for more complex queries
## Upgrading from v3
There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details.
## Getting Started ## Getting Started
Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library: Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library:
@@ -72,28 +77,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, Field, Task<long>> // Count.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task<long>>
var customerCount = await Count.ByField("customer", Field.Equal("City", "Atlanta")); var customerCount = await Count.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Atlanta")]);
``` ```
```fsharp ```fsharp
// F# // F#
// Count.byField type signature is string -> Field -> Task<int64> // Count.byFields type signature is string -> FieldMatch -> Field seq -> Task<int64>
let! customerCount = Count.byField "customer" (Field.Equal "City" "Atlanta") let! customerCount = Count.byFields "customer" Any [ Field.Equal "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, Field, Task> // Delete.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task>
await Delete.ByField("customer", Field.Equal("City", "Chicago")); await Delete.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Chicago")]);
``` ```
```fsharp ```fsharp
// F# // F#
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit> // Delete.byFields type signature is string -> FieldMatch -> Field seq -> Task<unit>
do! Delete.byField "customer" (Field.Equal "City" "Chicago") do! Delete.byFields "customer" Any [ Field.Equal "City" "Chicago" ]
``` ```
## More Information ## More Information

View File

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

View File

@@ -97,14 +97,20 @@ let fieldTests = testList "Field" [
test "In succeeds" { test "In succeeds" {
let field = Field.In "Here" [| 8; 16; 32 |] let field = Field.In "Here" [| 8; 16; 32 |]
Expect.equal field.Name "Here" "Field name incorrect" Expect.equal field.Name "Here" "Field name incorrect"
Expect.equal field.Comparison (In [| 8; 16; 32 |]) "Comparison incorrect" match field.Comparison with
| In values -> Expect.equal (List.ofSeq values) [ box 8; box 16; box 32 ] "Comparison incorrect"
| it -> Expect.isTrue false $"Expected In, received %A{it}"
Expect.isNone field.ParameterName "The default parameter name should be None" Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None" Expect.isNone field.Qualifier "The default table qualifier should be None"
} }
test "InArray succeeds" { test "InArray succeeds" {
let field = Field.InArray "ArrayField" "table" [| "z" |] let field = Field.InArray "ArrayField" "table" [| "z" |]
Expect.equal field.Name "ArrayField" "Field name incorrect" Expect.equal field.Name "ArrayField" "Field name incorrect"
Expect.equal field.Comparison (InArray("table", [| "z" |])) "Comparison incorrect" match field.Comparison with
| InArray (table, values) ->
Expect.equal table "table" "Comparison table incorrect"
Expect.equal (List.ofSeq values) [ box "z" ] "Comparison values incorrect"
| it -> Expect.isTrue false $"Expected InArray, received %A{it}"
Expect.isNone field.ParameterName "The default parameter name should be None" Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None" Expect.isNone field.Qualifier "The default table qualifier should be None"
} }

View File

@@ -7,8 +7,8 @@ dotnet build BitBadger.Documents.sln --no-restore
cd ./Tests || exit cd ./Tests || exit
export BBDOX_PG_PORT=8301 export BBDOX_PG_PORT=8301
PG_VERSIONS=('12' '13' '14' '15' 'latest') PG_VERSIONS=('13' '14' '15' '16' 'latest')
NET_VERSIONS=('6.0' '8.0') NET_VERSIONS=('8.0' '9.0')
for PG_VERSION in "${PG_VERSIONS[@]}" for PG_VERSION in "${PG_VERSIONS[@]}"
do do