Compare commits

..

No commits in common. "main" and "v3.1" have entirely different histories.
main ... v3.1

53 changed files with 6531 additions and 23166 deletions

8
.gitignore vendored
View File

@ -396,11 +396,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
**/.idea
# Test run files
src/*-tests.txt
# Documentation builds and intermediate files
_site/
api/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,4 +0,0 @@
article h2 {
border-bottom: solid 1px gray;
margin-bottom: 1rem;
}

View File

@ -1,10 +0,0 @@
export default {
defaultTheme: "auto",
iconLinks: [
{
icon: "git",
href: "https://git.bitbadger.solutions/bit-badger/BitBadger.Documents",
title: "Source Repository"
}
]
}

View File

@ -1,59 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
"metadata": [
{
"src": [
{
"src": "./src",
"files": [
"Common/bin/Release/net9.0/*.dll",
"Postgres/bin/Release/net9.0/*.dll",
"Sqlite/bin/Release/net9.0/*.dll"
]
}
],
"dest": "api",
"properties": {
"TargetFramework": "net9.0"
}
}
],
"build": {
"content": [
{
"files": [
"index.md",
"toc.yml",
"api/**/*.{md,yml}",
"docs/**/*.{md,yml}"
],
"exclude": [
"_site/**"
]
}
],
"resource": [
{
"files": [
"bitbadger-doc.png",
"favicon.ico"
]
}
],
"output": "_site",
"template": [
"default",
"modern",
"doc-template"
],
"globalMetadata": {
"_appName": "BitBadger.Documents",
"_appTitle": "BitBadger.Documents",
"_appLogoPath": "bitbadger-doc.png",
"_appFaviconPath": "favicon.ico",
"_appFooter": "Hand-crafted documentation created with <a href=https://dotnet.github.io/docfx target=_blank class=external>docfx</a> by <a href=https://bitbadger.solutions target=_blank class=external>Bit Badger Solutions</a>",
"_enableSearch": true,
"pdf": false
}
}
}

View File

@ -1,38 +0,0 @@
# Custom Serialization
_<small>Documentation pages for `BitBadger.Npgsql.Documents` redirect here. This library replaced it as of v3; see project home if this applies to you.</small>_
JSON documents are sent to and received from both PostgreSQL and SQLite as `string`s; the translation to and from your domain objects (commonly called <abbr title="Plain Old CLR Objects">POCO</abbr>s) is handled via .NET. By default, the serializer used by the library is based on `System.Text.Json` with [converters for common F# types][fs].
## Implementing a Custom Serializer
`IDocumentSerializer` (found in the `BitBadger.Documents` namespace) specifies two methods. `Serialize<T>` takes a `T` and returns a `string`; `Deserialize<T>` takes a `string` and returns an instance of `T`. (These show as `'T` in F#.) While implementing those two methods is required, the custom implementation can use whatever library you desire, and contain converters for custom types.
Once this serializer is implemented and constructed, provide it to the library:
```csharp
// C#
var serializer = /* constructed serializer */;
Configuration.UseSerializer(serializer);
```
```fsharp
// F#
let serializer = (* constructed serializer *)
Configuration.useSerializer serializer
```
The biggest benefit to registering a serializer (apart from control) is that all JSON operations will use the same serializer. This is most important for PostgreSQL's JSON containment queries; the object you pass as the criteria will be translated properly before it is compared. However, "unstructured" data does not mean "inconsistently structured" data; if your application uses custom serialization, extending this to your documents ensures that the structure is internally consistent.
## Uses for Custom Serialization
- If you use a custom serializer (or serializer options) in your application, a custom serializer implementation can utilize these existing configuration options.
- If you prefer [`Newtonsoft.Json`][nj], you can wrap `JsonConvert` or `JsonSerializer` calls in a custom converter. F# users may consider incorporating Microsoft's [`FSharpLu.Json`][fj] converter.
- If your project uses [`NodaTime`][], your custom serializer could include its converters for `System.Text.Json` or `Newtonsoft.Json`.
- If you use <abbr title="Domain Driven Design">DDD</abbr> to define custom types, you can implement converters to translate them to/from your preferred JSON representation.
[fs]: https://github.com/Tarmil/FSharp.SystemTextJson "FSharp.SystemTextJson • GitHub"
[nj]: https://www.newtonsoft.com/json "Json.NET"
[fj]: https://github.com/microsoft/fsharplu/blob/main/FSharpLu.Json.md "FSharpLu.Json • GitHub"
[`NodaTime`]: https://nodatime.org/ "NodaTime"

View File

@ -1,16 +0,0 @@
# Advanced Usage
_<small>Documentation pages for `BitBadger.Npgsql.Documents` redirect here. This library replaced it as of v3; see project home if this applies to you.</small>_
While the functions provided by the library cover lots of use cases, there are other times when applications need something else. Below are some of those.
- [Customizing Serialization][ser]
- [Related Documents and Custom Queries][rel]
- [Transactions][txn]
- [Referential Integrity with Documents][ref] (PostgreSQL only; conceptual)
[ser]: ./custom-serialization.md "Advanced Usage: Custom Serialization • BitBadger.Documents"
[rel]: ./related.md "Advanced Usage: Related Documents • BitBadger.Documents"
[txn]: ./transactions.md "Advanced Usage: Transactions • BitBadger.Documents"
[ref]: /concepts/referential-integrity.html "Appendix: Referential Integrity with Documents &bull; Concepts &bull; Relationanl Documents"

View File

@ -1,379 +0,0 @@
# Related Documents and Custom Queries
_<small>Documentation pages for `BitBadger.Npgsql.Documents` redirect here. This library replaced it as of v3; see project home if this applies to you.</small>_
_NOTE: This page is longer than the ideal documentation page. Understanding how to assemble custom queries requires understanding how data is stored, and the list of ways to retrieve information can be... a lot. The hope is that one reading will serve as education, and the lists of options will serve as reference lists that will assist you in crafting your queries._
## Overview
Document stores generally have fewer relationships than traditional relational databases, particularly those that arise when data is structured in [Third Normal Form][tnf]; related collections are stored in the document, and ever-increasing surrogate keys (_a la_ sequences and such) do not play well with distributed data. Unless all data is stored in a single document, though, there will still be a natural relation between documents.
Thinking back to our earlier examples, we did not store the collection of rooms in each hotel's document; each room is its own document and contains the ID of the hotel as one of its properties.
```csharp
// C#
public class Hotel
{
public string Id { get; set; } = "";
// ... more properties
}
public class Room
{
public string Id { get; set; } = "";
public string HotelId { get; set; } = "";
// ... more properties
}
```
```fsharp
// F#
[<CLIMutable>]
type Hotel =
{ Id: string
// ... more fields
}
[<CLIMutable>]
type Room =
{ Id: string
HotelId: string
// ... more fields
}
```
> The `CLIMutable` attribute is required on record types that are instantiated by the <abbr title="Common Language Runtime">CLR</abbr>; this attribute generates a zero-parameter constructor.
## Document Table SQL in Depth
The library creates tables with a `data` column of type `JSONB` (PostgreSQL) or `TEXT` (SQLite), with a unique index on the configured ID name that serves as the primary key (for these examples, we'll assume it's the default `Id`). The indexes created by the library all apply to the `data` column. The by-ID query for a hotel would be...
```sql
SELECT data FROM hotel WHERE data->>'Id' = @id
```
...with the ID passed as the `@id` parameter.
> _Using a "building block" method/function `Query.WhereById` will create the `data->>'Id' = @id` criteria using [the configured ID name][id]._
Finding all the rooms for a hotel, using our indexes we created earlier, could use a field comparison query...
```sql
SELECT data FROM room WHERE data->>'HotelId' = @field
```
...with `@field` being "abc123"; PostgreSQL could also use a JSON containment query...
```sql
SELECT data FROM room WHERE data @> @criteria
```
...with something like `new { HotelId = "abc123" }` passed as the matching document in the `@criteria` parameter.
So far, so good; but, if we're looking up a room, we do not want to have to make 2 queries just to also be able to display the hotel's name. The `WHERE` clause on the first query above uses the expression `data->>'Id'`; this extracts a field from a JSON column as `TEXT` in PostgreSQL (or "best guess" in SQLite, but usually text). Since this is the value our unique index indexes, and we are using a relational database, we can write an efficient JOIN between these two tables.
```sql
SELECT r.data, h.data AS hotel_data
FROM room r
INNER JOIN hotel h ON h.data->>'Id' = r.data->>'HotelId'
WHERE r.data->>'Id' = @id
```
_(This syntax would work without the unique index; for PostgreSQL, it would default to using the GIN index (`Full` or `Optimized`), if it exists, but it wouldn't be quite as efficient as a zero-or-one unique index lookup. For SQLite, this would result in a full table scan. Both PostgreSQL and SQLite also support a `->` operator, which extracts the field as a JSON value instead of its text.)_
## Using Building Blocks
Most of the data access methods in both libraries are built up from query fragments and reusable functions; these are exposed for use in building custom queries.
### Queries
For every method or function described in [Basic Usage][], the `Query` static class/module contains the building blocks needed to construct query for that operation. Both the parent and implementation namespaces have a `Query` module; in C#, you'll need to qualify the implementation module namespace.
In `BitBadger.Documents.Query`, you'll find:
- **StatementWhere** takes a SQL statement and a `WHERE` clause and puts them together on either side of the text ` WHERE `
- **Definition** contains methods/functions to ensure tables, their keys, and field indexes exist.
- **Insert**, **Save**, **Count**, **Find**, **Update**, and **Delete** are the prefixes of the queries for those actions; they all take a table name and return this query (with no `WHERE` clause)
- **Exists** also requires a `WHERE` clause, due to how the query is constructed
because it is inserted as a subquery
Within each implementation's `Query` module:
- **WhereByFields** takes a `FieldMatch` case and a set of fields. `Field` has constructor functions for each comparison it supports; these functions generally take a field name and a value, though the latter two do not require a value.
- **Equal** uses `=` to create an equality comparison
- **Greater** uses `>` to create a greater-than comparison
- **GreaterOrEqual** uses `>=` to create a greater-than-or-equal-to comparison
- **Less** uses `<` to create a less-than comparison
- **LessOrEqual** uses `<=` to create a less-than-or-equal-to comparison
- **NotEqual** uses `<>` to create a not-equal comparison
- **Between** uses `BETWEEN` to create a range comparison
- **In** uses `IN` to create an equality comparison within a set of given values
- **InArray** uses `?|` in PostgreSQL, and a combination of `EXISTS` / `json_each` / `IN` in SQLite, to create an equality comparison within a given set of values against an array in a JSON document
- **Exists** uses `IS NOT NULL` to create an existence comparison
- **NotExists** uses `IS NULL` to create a non-existence comparison; fields are considered null if they are either not part of the document, or if they are part of the document but explicitly set to `null`
- **WhereById** takes a parameter name and generates a field `Equal` comparison against the configured ID field.
- **Patch** and **RemoveFields** use each implementation's unique syntax for partial updates and field removals.
- **ByFields**, **ByContains** (PostgreSQL), and **ByJsonPath** (PostgreSQL) are functions that take a statement and the criteria, and construct a query to fit that criteria. For `ByFields`, each field parameter will use its specified name if provided (an incrementing `field[n]` if not). `ByContains` uses `@criteria` as its parameter name, which can be any object. `ByJsonPath` uses `@path`, which should be a `string`.
That's a lot of reading! Some examples a bit below will help this make sense.
### Parameters
Traditional ADO.NET data access involves creating a connection object, then adding parameters to that object. This library follows a more declarative style, where parameters are passed via `IEnumerable` collections. To assist with creating these collections, each implementation has some helper functions. For C#, these calls will need to be prefixed with `Parameters`; for F#, this module is auto-opened. This is one area where names differ in other than just casing, so both will be listed.
- **Parameters.Id** / **idParam** generate an `@id` parameter with the numeric, `string`, or `ToString()`ed value of the ID passed.
- **Parameters.Json** / **jsonParam** generate a user-provided-named JSON-formatted parameter for the value passed (this can be used for PostgreSQL's JSON containment queries as well)
- **Parameters.AddFields** / **addFieldParams** append field parameters to the given parameter list
- **Parameters.FieldNames** / **fieldNameParams** create parameters for the list of field names to be removed; for PostgreSQL, this returns a single parameter, while SQLite returns a list of parameters
- **Parameters.None** / **noParams** is an empty set of parameters, and can be cleaner and convey intent better than something like `new[] { }` _(For C# 12 or later, the collection expression `[]` is much terser.)_
If you need a parameter beyond these, both `NpgsqlParameter` and `SqliteParameter` have a name-and-value constructor; that isn't many more keystrokes.
### Results
The `Results` module is implementation specific. Both libraries provide `Results.FromData<T>`, which deserializes a `data` column into the requested type; and `FromDocument<T>`, which does the same thing, but allows the column to be named as well. We'll see how we can use these in further examples. As with parameters, C# users need to qualify the class name, but the module is auto-opened for F#.
## Putting It All Together
The **Custom** static class/module has seven methods/functions:
- **List** requires a query, parameters, and a mapping function, and returns a list of documents.
- **JsonArray** is the same as `List`, but returns the documents as `string` in a JSON array.
- **WriteJsonArray** writes documents to a `PipeWriter` as they are read from the database; the result is the same a `JsonArray`, but no unified strings is constructed.
- **Single** requires a query, parameters, and a mapping function, and returns one or no documents (C# `TDoc?`, F# `'TDoc option`)
- **JsonSingle** is the same as `Single`, but returns a JSON `string` instead (returning `{}` if no document is found).
- **Scalar** requires a query, parameters, and a mapping function, and returns a scalar value (non-nullable; used for counts, existence, etc.)
- **NonQuery** requires a query and parameters and has no return value
> _Within each library, every other call is written in terms of these functions; your custom queries will use the same code the provided ones do!_
Let's jump in with an example. When we query for a room, let's say that we also want to retrieve its hotel information as well. We saw the query above, but here is how we can implement it using a custom query.
```csharp
// C#, All
// return type is Tuple<Room, Hotel>?
var data = await Custom.Single(
$"SELECT r.data, h.data AS hotel_data
FROM room r
INNER JOIN hotel h ON h.data->>'{Configuration.IdField()}' = r.data->>'HotelId'
WHERE r.{Query.WhereById("@id")}",
new[] { Parameters.Id("my-room-key") },
// rdr's type will be RowReader for PostgreSQL, SqliteDataReader for SQLite
rdr => Tuple.Create(Results.FromData<Room>(rdr), Results.FromDocument<Hotel>("hotel_data", rdr));
if (data is not null)
{
var (room, hotel) = data;
// do stuff with the room and hotel data
}
```
```fsharp
// F#, All
// return type is (Room * Hotel) option
let! data =
Custom.single
$"""SELECT r.data, h.data AS hotel_data
FROM room r
INNER JOIN hotel h ON h.data->>'{Configuration.idField ()}' = r.data->>'HotelId'
WHERE r.{Query.whereById "@id"}"""
[ idParam "my-room-key" ]
// rdr's type will be RowReader for PostgreSQL, SqliteDataReader for SQLite
fun rdr -> (fromData<Room> rdr), (fromDocument<Hotel> "hotel_data" rdr)
match data with
| Some (Room room, Hotel hotel) ->
// do stuff with room and hotel
| None -> ()
```
These queries use `Configuration.IdField` and `WhereById` to use the configured ID field. Creating custom queries using these building blocks allows us to utilize the configured value without hard-coding it throughout our custom queries. If the configuration changes, these queries will pick up the new field name seamlessly.
While this example retrieves the entire document, this is not required. If we only care about the name of the associated hotel, we could amend the query to retrieve only that information.
```csharp
// C#, All
// return type is Tuple<Room, string>?
var data = await Custom.Single(
$"SELECT r.data, h.data ->> 'Name' AS hotel_name
FROM room r
INNER JOIN hotel h ON h.data->>'{Configuration.IdField()}' = r.data->>'HotelId'
WHERE r.{Query.WhereById("@id")}",
new[] { Parameters.Id("my-room-key") },
// PostgreSQL
row => Tuple.Create(Results.FromData<Room>(row), row.string("hotel_name")));
// SQLite; could use rdr.GetString(rdr.GetOrdinal("hotel_name")) below as well
// rdr => Tuple.Create(Results.FromData<Room>(rdr), rdr.GetString(1)));
if (data is not null)
{
var (room, hotelName) = data;
// do stuff with the room and hotel name
}
```
```fsharp
// F#, All
// return type is (Room * string) option
let! data =
Custom.single
$"""SELECT r.data, h.data->>'Name' AS hotel_name
FROM room r
INNER JOIN hotel h ON h.data->>'{Configuration.idField ()}' = r.data->>'HotelId'
WHERE r.{Query.whereById "@id"}"""
[ idParam "my-room-key" ]
// PostgreSQL
fun row -> (fromData<Room> row), row.string "hotel_name"
// SQLite; could use rdr.GetString(rdr.GetOrdinal("hotel_name")) below as well
// fun rdr -> (fromData<Room> rdr), rdr.GetString(1)
match data with
| Some (Room room, string hotelName) ->
// do stuff with room and hotel name
| None -> ()
```
These queries are amazingly efficient, using 2 unique index lookups to return this data. Even though we do not have a foreign key between these two tables, simply being in a relational database allows us to retrieve this related data.
Revisiting our "take these rooms out of service" SQLite query from the Basic Usage page, here's how that could look using building blocks available since version 4 (PostgreSQL will accept this query syntax as well, though the parameter types would be different):
```csharp
// C#, SQLite
var fields = [Field.GreaterOrEqual("RoomNumber", 221), Field.LessOrEqual("RoomNumber", 240)];
await Custom.NonQuery(
Sqlite.Query.ByFields(Sqlite.Query.Patch("room"), FieldMatch.All, fields,
new { InService = false }),
Parameters.AddFields(fields, []));
```
```fsharp
// F#, SQLite
let fields = [ Field.GreaterOrEqual "RoomNumber" 221; Field.LessOrEqual "RoomNumber" 240 ]
do! Custom.nonQuery
(Query.byFields (Query.patch "room") All fields {| InService = false |})
(addFieldParams fields []))
```
This uses two field comparisons to incorporate the room number range instead of a `BETWEEN` clause; we would definitely want to have that field indexed if this was going to be a regular query or our data was going to grow beyond a trivial size.
_You may be thinking "wait - what's the difference between that an the regular `Patch` call?" And you'd be right; that is exactly what `Patch.ByFields` does. `Between` is also a better comparison for this, and either `FieldMatch` type will work, as we're only passing one field. No building blocks required!_
```csharp
// C#, All
await Patch.ByFields("room", FieldMatch.Any, [Field.Between("RoomNumber", 221, 240)],
new { InService = false });
```
```fsharp
// F#, All
do! Patch.byFields "room" Any [ Field.Between "RoomNumber" 221 240 ] {| InService = false |}
```
## Going Even Further
### Updating Data in Place
One drawback to document databases is the inability to update values in place; however, with a bit of creativity, we can do a lot more than we initially think. For a single field, SQLite has a `json_set` function that takes an existing JSON field, a field name, and a value to which it should be set. This allows us to do single-field updates in the database. If we wanted to raise our rates 10% for every room, we could use this query:
```sql
-- SQLite
UPDATE room SET data = json_set(data, 'Rate', data->>'Rate' * 1.1)
```
If we get any more complex, though, Common Table Expressions (CTEs) can help us. Perhaps we decided that we only wanted to raise the rates for hotels in New York, Chicago, and Los Angeles, and we wanted to exclude any brand with the word "Value" in its name. A CTE lets us select the source data we need to craft the update, then use that in the `UPDATE`'s clauses.
```sql
-- SQLite
WITH to_update AS
(SELECT r.data->>'Id' AS room_id, r.data->>'Rate' AS current_rate, r.data AS room_data
FROM room r
INNER JOIN hotel h ON h.data->>'Id' = r.data->>'HotelId'
WHERE h.data->>'City' IN ('New York', 'Chicago', 'Los Angeles')
AND LOWER(h.data->>'Name') NOT LIKE '%value%')
UPDATE room
SET data = json_set(to_update.room_data, 'Rate', to_update.current_rate * 1.1)
WHERE room->>'Id' = to_update.room_id
```
Both PostgreSQL and SQLite provide JSON patching, where multiple fields (or entire structures) can be changed at once. Let's revisit our rate increase; if we are making the rate more than $500, we'll apply a status of "Premium" to the room. If it is less than that, it should keep its same value.
First up, PostgreSQL:
```sql
-- PostgreSQL
WITH to_update AS
(SELECT r.data->>'Id' AS room_id, (r.data->>'Rate')::decimal AS rate, r.data->>'Status' AS status
FROM room r
INNER JOIN hotel h ON h.data->>'Id' = r.data->>'HotelId'
WHERE h.data->>'City' IN ('New York', 'Chicago', 'Los Angeles')
AND LOWER(h.data ->> 'Name') NOT LIKE '%value%')
UPDATE room
SET data = data ||
('{"Rate":' || to_update.rate * 1.1 || '","Status":"'
|| CASE WHEN to_update.rate * 1.1 > 500 THEN 'Premium' ELSE to_update.status END
|| '"}')
WHERE room->>'Id' = to_update.room_id
```
In SQLite:
```sql
-- SQLite
WITH to_update AS
(SELECT r.data->>'Id' AS room_id, r.data->>'Rate' AS rate, r.data->>'Status' AS status
FROM room r
INNER JOIN hotel h ON h.data->>'Id' = r.data->>'HotelId'
WHERE h.data->>'City' IN ('New York', 'Chicago', 'Los Angeles')
AND LOWER(h.data->>'Name') NOT LIKE '%value%')
UPDATE room
SET data = json_patch(data, json(
'{"Rate":' || to_update.rate * 1.1 || '","Status":"'
|| CASE WHEN to_update.rate * 1.1 > 500 THEN 'Premium' ELSE to_update.status END
|| '"}'))
WHERE room->>'Id' = to_update.room_id
```
For PostgreSQL, `->>` always returns text, so we need to cast the rate to a number. In either case, we do not want to use this technique for user-provided data; however, in place, it allowed us to complete all of our scenarios without having to load the documents into our application and manipulate them there.
Updates in place may not need parameters (though it would be easy to foresee a "rate adjustment" feature where the 1.1 adjustment was not hard-coded); in fact, none of the samples in this section used the document libraries at all. These queries can be executed by `Custom.NonQuery`, though, providing parameters as required.
### Using This Library for Non-Document Queries
The `Custom` methods/functions can be used with non-document tables as well. This may be a convenient and consistent way to access your data, while delegating connection management to the library and its configured data source.
Let's walk through a short example using C# and PostgreSQL:
```csharp
// C#, PostgreSQL
using Npgsql.FSharp; // Needed for RowReader and Sql types
using static CommonExtensionsAndTypesForNpgsqlFSharp; // Needed for Sql functions
// Stores metadata for a given user
public class MetaData
{
public string Id { get; set; } = "";
public string UserId { get; set; } = "";
public string Key { get; set; } = "";
public string Value { get; set; } = "";
}
// Static class to hold mapping functions
public static class Map
{
// These parameters are the column names from the underlying table
public MetaData ToMetaData(RowReader row) =>
new MetaData
{
Id = row.string("id"),
UserId = row.string("user_id"),
Key = row.string("key"),
Value = row.string("value")
};
}
// somewhere in a class, retrieving data
public Task<List<MetaData>> MetaDataForUser(string userId) =>
Document.Custom.List("SELECT * FROM user_metadata WHERE user_id = @userId",
new { Tuple.Create("@userId", Sql.string(userId)) },
Map.ToMetaData);
```
For F#, the `using static` above is not needed; that module is auto-opened when `Npgsql.FSharp` is opened. For SQLite in either language, the mapping function uses a `SqliteDataReader` object, which implements the standard ADO.NET `DataReader` functions of `Get[Type](idx)` (and `GetOrdinal(name)` for the column index).
[tnf]: https://en.wikipedia.org/wiki/Third_normal_form "Third Normal Form • Wikipedia"
[id]: ../getting-started.md#field-name "Getting Started (ID Fields) • BitBadger.Documents"
[Basic Usage]: ../basic-usage.md "Basic Usage • BitBadger.Documents"

View File

@ -1,96 +0,0 @@
# Transactions
_<small>Documentation pages for `BitBadger.Npgsql.Documents` redirect here. This library replaced it as of v3; see project home if this applies to you.</small>_
On occasion, there may be a need to perform multiple updates in a single database transaction, where either all updates succeed, or none do.
## Controlling Database Transactions
The `Configuration` static class/module of each library [provides a way to obtain a connection][conn]. Whatever strategy your application uses to obtain the connection, the connection object is how ADO.NET implements transactions.
```csharp
// C#, All
// "conn" is assumed to be either NpgsqlConnection or SqliteConnection
await using var txn = await conn.BeginTransactionAsync();
try
{
// do stuff
await txn.CommitAsync();
}
catch (Exception ex)
{
await txn.RollbackAsync();
// more error handling
}
```
```fsharp
// F#, All
// "conn" is assumed to be either NpgsqlConnection or SqliteConnection
use! txn = conn.BeginTransactionAsync ()
try
// do stuff
do! txn.CommitAsync ()
with ex ->
do! txt.RollbackAsync ()
// more error handling
```
## Executing Queries on the Connection
This precise scenario was the reason that all methods and functions are implemented on the connection object; all extensions execute the commands in the context of the connection. Imagine an application where a user signs in. We may want to set an attribute on the user record that says that now is the last time they signed in; and we may also want to reset a failed logon counter, as they have successfully signed in. This would look like:
```csharp
// C#, All ("conn" is our connection object)
await using var txn = await conn.BeginTransactionAsync();
try
{
await conn.PatchById("user_table", userId, new { LastSeen = DateTime.Now });
await conn.PatchById("security", userId, new { FailedLogOnCount = 0 });
await txn.CommitAsync();
}
catch (Exception ex)
{
await txn.RollbackAsync();
// more error handling
}
```
```fsharp
// F#, All ("conn" is our connection object)
use! txn = conn.BeginTransactionAsync()
try
do! conn.patchById "user_table" userId {| LastSeen = DateTime.Now |}
do! conn.patchById "security" userId {| FailedLogOnCount = 0 |}
do! txn.CommitAsync()
with ex ->
do! txn.RollbackAsync()
// more error handling
```
### A Functional Alternative
The PostgreSQL library has a static class/module called `WithProps`; the SQLite library has a static class/module called `WithConn`. Each of these accept the `SqlProps` or `SqliteConnection` parameter as the last parameter of the query. For SQLite, we need nothing else to pass the connection to these methods/functions; for PostgreSQL, though, we'll need to create a `SqlProps` object based off the connection.
```csharp
// C#, PostgreSQL
using Npgsql.FSharp;
// ...
var props = Sql.existingConnection(conn);
// ...
await WithProps.Patch.ById("user_table", userId, new { LastSeen = DateTime.Now }, props);
```
```fsharp
// F#, PostgreSQL
open Npgsql.FSharp
// ...
let props = Sql.existingConnection conn
// ...
do! WithProps.Patch.ById "user_table" userId {| LastSeen = DateTime.Now |} props
```
If we do not want to qualify with `WithProps` or `WithConn`, C# users can add `using static [WithProps|WithConn];` to bring these functions into scope; F# users can add `open BitBadger.Documents.[Postgres|Sqlite].[WithProps|WithConn]` to bring them into scope. However, in C#, this will affect the entire file, and in F#, it will affect the file from that point through the end of the file. Unless you want to go all-in with the connection-last functions, it is probably better to qualify the occasional call.
[conn]: ../getting-started.md#the-connection "Getting Started (The Connection) • BitBadger.Documents"

View File

@ -1,149 +0,0 @@
# Basic Usage
_<small>Documentation pages for `BitBadger.Npgsql.Documents` redirect here. This library replaced it as of v3; see project home if this applies to you.</small>_
## Overview
There are several categories of operations that can be accomplished against documents.
- **Count** returns the number of documents matching some criteria
- **Exists** returns true if any documents match the given criteria
- **Insert** adds a new document, failing if the ID field is not unique
- **Save** adds a new document, updating an existing one if the ID is already present ("upsert")
- **Update** updates an existing document, doing nothing if no documents satisfy the criteria
- **Patch** updates a portion of an existing document, doing nothing if no documents satisfy the criteria
- **Find** returns the documents matching some criteria as domain objects
- **Json** returns or writes documents matching some criteria as JSON text
- **RemoveFields** removes fields from documents matching some criteria
- **Delete** removes documents matching some criteria
`Insert` and `Save` were the only two that don't mention criteria. For the others, "some criteria" can be defined a few different ways:
- **All** references all documents in the table; applies to Count and Find
- **ById** looks for a single document on which to operate; applies to all but Count
- **ByFields** uses JSON field comparisons to select documents for further processing (PostgreSQL will use a numeric comparison if the field value is numeric, or a string comparison otherwise; SQLite will do its usual [best-guess on types][]{target=_blank rel=noopener}); applies to all but Update
- **ByContains** (PostgreSQL only) uses a JSON containment query (the `@>` operator) to find documents where the given sub-document occurs (think of this as an `=` comparison based on one or more properties in the document; looking for hotels with `{ "Country": "USA", "Rating": 4 }` would find all hotels with a rating of 4 in the United States); applies to all but Update
- **ByJsonPath** (PostgreSQL only) uses a JSON patch match query (the `@?` operator) to make specific queries against a document's structure (it also supports more operators than a containment query; to find all hotels rated 4 _or higher_ in the United States, we could query for `"$ ? (@.Country == \"USA\" && @.Rating > 4)"`); applies to all but Update
Finally, `Find` and `Json` also have `FirstBy*` implementations for all supported criteria types, and `Find*Ordered` implementations to sort the results in the database.
## Saving Documents
The library provides three different ways to save data. The first equates to a SQL `INSERT` statement, and adds a single document to the repository.
```csharp
// C#, All
var room = new Room(/* ... */);
// Parameters are table name and document
await Document.Insert("room", room);
```
```fsharp
// F#, All
let room = { Room.empty with (* ... *) }
do! insert "room" room
```
The second is `Save`; and inserts the data it if does not exist and replaces the document if it does exist (what some call an "upsert"). It utilizes the `ON CONFLICT` syntax to ensure an atomic statement. Its parameters are the same as those for `Insert`.
The third equates to a SQL `UPDATE` statement. `Update` applies to a full document and is usually used by ID, while `Patch` is used for partial updates and may be done by field comparison, JSON containment, or JSON Path match. For a few examples, let's begin with a query that may back the "edit hotel" page. This page lets the user update nearly all the details for the hotel, so updating the entire document would be appropriate.
```csharp
// C#, All
var hotel = await Document.Find.ById<Hotel>("hotel", hotelId);
if (!(hotel is null))
{
// update hotel properties from the posted form
await Update.ById("hotel", hotel.Id, hotel);
}
```
```fsharp
// F#, All
match! Find.byId<Hotel> "hotel" hotelId with
| Some hotel ->
do! Update.byId "hotel" hotel.Id updated
{ hotel with (* properties from posted form *) }
| None -> ()
```
For the next example, suppose we are upgrading our hotel, and need to take rooms 221-240 out of service*. We can utilize a patch via JSON Path** to accomplish this.
```csharp
// C#, PostgreSQL
await Patch.ByJsonPath("room",
"$ ? (@.HotelId == \"abc\" && (@.RoomNumber >= 221 && @.RoomNumber <= 240)",
new { InService = false });
```
```fsharp
// F#, PostgreSQL
do! Patch.byJsonPath "room"
"$ ? (@.HotelId == \"abc\" && (@.RoomNumber >= 221 && @.RoomNumber <= 240)"
{| InService = false |};
```
_* - we are ignoring the current reservations, end date, etc. This is very naïve example!_
\** - Both PostgreSQL and SQLite can also accomplish this using the `Between` comparison and a `ByFields` query:
```csharp
// C#, Both
await Patch.ByFields("room", FieldMatch.Any, [Field.Between("RoomNumber", 221, 240)],
new { InService = false });
```
```fsharp
// F#, Both
do! Patch.byFields "room" Any [ Field.Between "RoomNumber" 221 240 ] {| InService = false |}
```
This could also be done with `All`/`FieldMatch.All` and `GreaterOrEqual` and `LessOrEqual` field comparisons, or even a custom query; these are fully explained in the [Advanced Usage][] section.
> There is an `Update.ByFunc` variant that takes an ID extraction function run against the document instead of its ID. This is detailed in the [Advanced Usage][] section.
## Finding Documents as Domain Items
Functions to find documents start with `Find.`. There are variants to find all documents in a table, find by ID, find by JSON field comparisons, find by JSON containment, or find by JSON Path. The hotel update example above utilizes an ID lookup; the descriptions of JSON containment and JSON Path show examples of the criteria used to retrieve using those techniques.
`Find` methods and functions are generic; specifying the return type is crucial. Additionally, `ById` will need the type of the key being passed. In C#, `ById` and the `FirstBy*` methods will return `TDoc?`, with the value if it was found or `null` if it was not; `All` and other `By*` methods return `List<TDoc>` (from `System.Collections.Generic`). In F#, `byId` and the `firstBy*` functions will return `'TDoc option`; `all` and other `by*` functions return `'TDoc list`.
`Find*Ordered` methods and function append an `ORDER BY` clause to the query that will sort the results in the database. These take, as their last parameter, a sequence of `Field` items; a `.Named` method allows for field creation for these names. Within these names, prefixing the name with `n:` will tell PostgreSQL to sort this field numerically rather than alphabetically; it has no effect in SQLite (it does its own [type coercion][best-guess on types]). Adding " DESC" at the end will sort high-to-low instead of low-to-high.
## Finding Documents as JSON
All `Find` methods and functions have two corresponding `Json` functions.
* The first set return the expected document(s) as a `string`, and will always return valid JSON. Single-document queries with nothing found will return `{}`, while zero-to-many queries will return `[]` if no documents match the given criteria.
* The second set are prefixed with `Write`, and take a `PipeWriter` immediately after the table name parameter. These functions write results to the given pipeline as they are retrieved from the database, instead of accumulating them all and returning a `string`. This can be useful for JSON API scenarios; ASP.NET Core's `HttpResponse.BodyWriter` property is a `PipeWriter` (and pipelines are [preferred over streams][pipes]).
## Deleting Documents
Functions to delete documents start with `Delete.`. Document deletion is supported by ID, JSON field comparison, JSON containment, or JSON Path match. The pattern is the same as for finding or partially updating. _(There is no library method provided to delete all documents, though deleting by JSON field comparison where a non-existent field is null would accomplish this.)_
## Counting Documents
Functions to count documents start with `Count.`. Documents may be counted by a table in its entirety, by JSON field comparison, by JSON containment, or by JSON Path match. _(Counting by ID is an existence check!)_
## Document Existence
Functions to check for existence start with `Exists.`. Documents may be checked for existence by ID, JSON field comparison, JSON containment, or JSON Path match.
## What / How Cross-Reference
The table below shows which commands are available for each access method. (X = supported for both, P = PostgreSQL only)
| Operation | `All` | `ById` | `ByFields` | `ByContains` | `ByJsonPath` | `FirstByFields` | `FirstByContains` | `FirstByJsonPath` |
|-----------------|:-----:|:------:|:----------:|:------------:|:------------:|:---------------:|:-----------------:|:-----------------:|
| `Count` | X | | X | P | P | | | |
| `Exists` | | X | X | P | P | | | |
| `Find` / `Json` | X | X | X | P | P | X | P | P |
| `Patch` | | X | X | P | P | | | |
| `RemoveFields` | | X | X | P | P | | | |
| `Delete` | | X | X | P | P | | | |
`Insert`, `Save`, and `Update.*` operate on single documents.
[best-guess on types]: https://sqlite.org/datatype3.html "Datatypes in SQLite • SQLite"
[JSON Path]: https://www.postgresql.org/docs/15/functions-json.html#FUNCTIONS-SQLJSON-PATH "JSON Functions and Operators • PostgreSQL Documentation"
[Advanced Usage]: ./advanced/index.md "Advanced Usage • BitBadger.Documents • Bit Badger Solutions"
[pipes]: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/request-response?view=aspnetcore-9.0 "Request and Response Operations &bull; Microsoft Learn"

View File

@ -1,187 +0,0 @@
# Getting Started
## Overview
Each library has three different ways to execute commands:
- Functions/methods that have no connection parameter at all; for these, each call obtains a new connection. _(Connection pooling greatly reduced this overhead and churn on the database)_
- Functions/methods that take a connection as the last parameter; these use the given connection to execute the commands.
- Extensions on the `NpgsqlConnection` or `SqliteConnection` type (native for both C# and F#); these are the same as the prior ones, and the names follow a similar pattern (ex. `Count.All()` is exposed as `conn.CountAll()`).
This provides flexibility in how connections are managed. If your application does not care about it, configuring the library is all that is required. If your application generally does not care, but needs a connection on occasion, one can be obtained from the library and used as required. If you are developing a web application, and want to use one connection per request, you can register the library's connection functions as a factory, and have that connection injected. We will cover the how-to below for each scenario, but it is worth considering before getting started.
> A note on functions: the F# functions use `camelCase`, while C# calls use `PascalCase`. To cut down on the noise, this documentation will generally use the C# `Count.All` form; know that this is `Count.all` for F#, `conn.CountAll()` for the C# extension method, and `conn.countAll` for the F# extension.
## Namespaces
### C#
```csharp
using BitBadger.Documents;
using BitBadger.Documents.[Postgres|Sqlite];
```
### F#
```fsharp
open BitBadger.Documents
open BitBadger.Documents.[Postgres|Sqlite]
```
For F#, this order is significant; both namespaces have modules that share names, and this order will control which one shadows the other.
## Configuring the Connection
### The Connection String
Both PostgreSQL and SQLite use the standard ADO.NET connection string format ([`Npgsql` docs][], [`Microsoft.Data.Sqlite` docs][]). The usual location for these is an `appsettings.json` file, which is then parsed into an `IConfiguration` instance. For SQLite, all the library needs is a connection string:
```csharp
// C#, SQLite
// ...
var config = ...; // parsed IConfiguration
Sqlite.Configuration.UseConnectionString(config.GetConnectionString("SQLite"));
// ...
```
```fsharp
// F#, SQLite
// ...
let config = ...; // parsed IConfiguration
Configuration.useConnectionString (config.GetConnectionString("SQLite"))
// ...
```
For PostgreSQL, the library needs an `NpgsqlDataSource` instead. There is a builder that takes a connection string and creates it, so it still is not a lot of code: _(although this implements `IDisposable`, do not declare it with `using` or `use`; the library handles disposal if required)_
```csharp
// C#, PostgreSQL
// ...
var config = ...; // parsed IConfiguration
var dataSource = new NpgsqlDataSourceBuilder(config.GetConnectionString("Postgres")).Build();
Postgres.Configuration.UseDataSource(dataSource);
// ...
```
```fsharp
// F#, PostgreSQL
// ...
let config = ...; // parsed IConfiguration
let dataSource = new NpgsqlDataSourceBuilder(config.GetConnectionString("Postgres")).Build()
Configuration.useDataSource dataSource
// ...
```
### The Connection
- If the application does not care to control the connection, use the methods/functions that do not require one.
- To retrieve an occasional connection (possibly to do multiple updates in a transaction), the `Configuration` static class/module for each implementation has a way. (For both of these, define the result with `using` or `use` so that they are disposed properly.)
- For PostgreSQL, the `DataSource()` method returns the configured `NpgsqlDataSource` instance; from this, `OpenConnection[Async]()` can be used to obtain a connection.
- For SQLite, the `DbConn()` method returns a new, open `SqliteConnection`.
- To use a connection per request in a web application scenario, register it with <abbr title="Dependency Injection">DI</abbr>.
```csharp
// C#, PostgreSQL
builder.Services.AddScoped<NpgsqlConnection>(svcProvider =>
Postgres.Configuration.DataSource().OpenConnection());
// C#, SQLite
builder.Services.AddScoped<SqliteConnection>(svcProvider => Sqlite.Configuration.DbConn());
```
```fsharp
// F#, PostgreSQL
let _ = builder.Services.AddScoped<NpgsqlConnection(fun sp -> Configuration.dataSource().OpenConnection())
// F#, SQLite
let _ = builder.Services.AddScoped<SqliteConnection>(fun sp -> Configuration.dbConn ())
```
After registering, this connection will be available on the request context and can be injected in the constructor for things like Razor Pages or MVC Controllers.
## Configuring Document IDs
### Field Name
A common .NET pattern when naming unique identifiers for entities / documents / etc. is the name `Id`. By default, this library assumes that this field is the identifier for your documents. If your code follows this pattern, you will be happy with the default behavior. If you use a different property, or [implement a custom serializer][ser] to modify the JSON representation of your documents' IDs, though, you will need to configure that field name before you begin calling other functions or methods. A great spot for this is just after you configure the connection string or data source (above). If you have decided that the field "Name" is the unique identifier for your documents, your setup would look something like...
```csharp
// C#, All
Configuration.UseIdField("Name");
```
```fsharp
// F#, All
Configuration.useIdField "Name"
```
Setting this will make `EnsureTable` create the unique index on that field when it creates a table, and will make all the `ById` functions and methods look for `data->>'Name'` instead of `data->>'Id'`. JSON is case-sensitive, so if the JSON is camel-cased, this should be configured to be `id` instead of `Id` (or `name` to follow the example above).
### Generation Strategy
The library can also generate IDs if they are missing. There are three different types of IDs, and each case of the `AutoId` enumeration/discriminated union can be passed to `Configuration.UseAutoIdStrategy()` to configure the library.
- `Number` generates a "max ID plus 1" query based on the current values of the table.
- `Guid` generates a 32-character string from a Globally Unique Identifier (GUID), lowercase with no dashes.
- `RandomString` generates random bytes and converts them to a lowercase hexadecimal string. By default, the string is 16 characters, but can be changed via `Configuration.UseIdStringLength()`. _(You can also use `AutoId.GenerateRandomString(length)` to generate these strings for other purposes; they make good salts, transient keys, etc.)_
All of these are off by default (the `Disabled` case). Even when ID generation is configured, though, only IDs of 0 (for `Number`) or empty strings (for `Guid` and `RandomString`) will be generated. IDs are only generated on `Insert`.
> Numeric IDs are a one-time decision. In PostgreSQL, once a document has a non-numeric ID, attempts to insert an automatic number will fail. One could switch from numbers to strings, and the IDs would be treated as such (`"33"` instead of `33`, for example). SQLite does a best-guess typing of columns, but once a string ID is there, the "max + 1" algorithm will not return the expected results.
## Ensuring Tables and Indexes Exist
Both PostgreSQL and SQLite store data in tables and can utilize indexes to retrieve that data efficiently. Each application will need to determine the tables and indexes it expects.
To discover these concepts, let's consider a naive example of a hotel chain; they have several hotels, and each hotel has several rooms. While each hotel could have its rooms as part of a `Hotel` document, there would likely be a lot of contention when concurrent updates for rooms, so we will put rooms in their own table. The hotel will store attributes like name, address, etc.; while each room will have the hotel's ID (named `Id`), along with things like room number, floor, and a list of date ranges where the room is not available. (This could be for customer reservation, maintenance, etc.)
_(Note that all "ensure" methods/functions below use the `IF NOT EXISTS` clause; they are safe to run each time the application starts up, and will do nothing if the tables or indexes already exist.)_
### PostgreSQL
We have a few options when it comes to indexing our documents. We can index a specific JSON field; each table's primary key is implemented as a unique index on the configured ID field. We can also use a <abbr title="Generalized Inverted Index">GIN</abbr> index to index the entire document, and that index can even be [optimized for a subset of JSON Path operators][json-index].
Let's create a general-purpose index on hotels, a "HotelId" index on rooms, and an optimized document index on rooms.
```csharp
// C#, Postgresql
await Definition.EnsureTable("hotel");
await Definition.EnsureDocumentIndex("hotel", DocumentIndex.Full);
await Definition.EnsureTable("room");
// parameters are table name, index name, and fields to be indexed
await Definition.EnsureFieldIndex("room", "hotel_id", ["HotelId"]);
await Definition.EnsureDocumentIndex("room", DocumentIndex.Optimized);
```
```fsharp
// F#, PostgreSQL
do! Definition.ensureTable "hotel"
do! Definition.ensureDocumentIndex "hotel" Full
do! Definition.ensureTable "room"
do! Definition.ensureFieldIndex "room" "hotel_id" [ "HotelId" ]
do! Definition.ensureDocumentIndex "room" Optimized
```
### SQLite
For SQLite, the only option for JSON indexes (outside some quite complex techniques) are indexes on fields. Just as traditional relational indexes, these fields can be specified in expected query order. In our example, if we indexed our rooms on hotel ID and room number, it could also be used for efficient retrieval just by hotel ID.
Let's create hotel and room tables, then index rooms by hotel ID and room number.
```csharp
// C#, SQLite
await Definition.EnsureTable("hotel");
await Definition.EnsureTable("room");
await Definition.EnsureIndex("room", "hotel_and_nbr", ["HotelId", "RoomNumber"]);
```
```fsharp
// F#
do! Definition.ensureTable "hotel"
do! Definition.ensureTable "room"
do! Definition.ensureIndex "room" "hotel_and_nbr", [ "HotelId"; "RoomNumber" ]
```
Now that we have tables, let's [use them][]!
[`Npgsql` docs]: https://www.npgsql.org/doc/connection-string-parameters "Connection String Parameter • Npgsql"
[`Microsoft.Data.Sqlite` docs]: https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/connection-strings "Connection Strings • Microsoft.Data.Sqlite • Microsoft Learn"
[ser]: ./advanced/custom-serialization.md "Advanced Usage: Custom Serialization • BitBadger.Documents"
[json-index]: https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING "Indexing JSON Fields &bull; PostgreSQL"
[use them]: ./basic-usage.md "Basic Usage • BitBadger.Documents"

View File

@ -1,21 +0,0 @@
- name: Getting Started
href: getting-started.md
- name: Basic Usage
href: basic-usage.md
- name: Advanced Usage
href: advanced/index.md
items:
- name: Custom Serialization
href: advanced/custom-serialization.md
- name: Related Documents and Custom Queries
href: advanced/related.md
- name: Transactions
href: advanced/transactions.md
- name: Upgrading
items:
- name: v3 to v4
href: upgrade/v4.md
- name: v2 to v3
href: upgrade/v3.md
- name: v1 to v2
href: upgrade/v2.md

View File

@ -1,37 +0,0 @@
# Migrating from v1 to v2
_NOTE: This was an upgrade for the `BitBadger.Npgsql.Documents` library, which this library replaced as of v3._
## Why
In version 1 of this library, the document tables used by this library had two columns: `id` and `data`. `id` served as the primary key, and `data` was the `JSONB` column for the document. Since its release, the author learned that a field in a `JSONB` column could have a unique index that would then serve the role of a primary key.
Version 2 of this library implements this change, both in table setup and in how it constructs queries that occur by a document's ID.
## How
On the [GitHub release page][], there is a MigrateToV2 utility program - one for Windows, and one for Linux. Download and extract the single file in the archive; it requires no installation. It uses an environment variable for the connection string, and takes a table name and an ID column field via the command line.
A quick example under Linux/bash (assuming the ID field in the JSON document is named `Id`)...
```
export PGDOC_CONN_STR="Host=localhost;Port=5432;User ID=example_user;Password=example_pw;Database=my_docs"
./MigrateToV2 ex.doc_table
./MigrateToV2 ex.another_one
```
If the ID field has a different name, it can be passed as a second parameter. The utility will display the table name and ID field and ask for confirmation; if you are scripting it, you can set the environment variable `PGDOC_I_KNOW_WHAT_I_AM_DOING` to `true`, and it will bypass this confirmation. Note that the utility itself is quite basic; you are responsible for giving it sane input. If you have customized the tables or the JSON serializer, though, keep reading.
## What
If you have extended the original tables, you may need to handle this migration within either PostgreSQL/psql or your code. The process entails two steps. First, create a unique index on the ID field; in this example, we'll use `name` for the example ID field. Then, drop the `id` column. The below SQL will accomplish this for the fictional `my_table` table.
```sql
CREATE UNIQUE INDEX idx_my_table_key ON my_table ((data ->> 'name'));
ALTER TABLE my_table DROP COLUMN id;
```
If the ID field is different, you will also need to tell the library that. Use `Configuration.UseIdField("name")` (C#) / `Configuration.useIdField "name"` (F#) to specify the name. This will need to be done before queries are executed, as the library uses this field for ID queries. See the [Setting Up instructions][setup] for details on this new configuration parameter.
[GitHub release page]: https://github.com/bit-badger/BitBadger.Npgsql.Documents
[setup]: ../getting-started.md#configuring-document-ids "Getting Started • BitBadger.Documents"

View File

@ -1,11 +0,0 @@
# Upgrade from v2 to v3
The biggest change with this release is that `BitBadger.Npgsql.Documents` became `BitBadger.Documents`, a set of libraries providing the same API over both PostgreSQL and SQLite (provided the underlying database supports it). Existing PostgreSQL users should have a smooth transition.
* Drop `Npgsql` from namespace (`BitBadger.Npgsql.Documents` becomes `BitBadger.Documents`)
* Add implementation (PostgreSQL namespace is `BitBadger.Documents.Postgres`, SQLite is `BitBadger.Documents.Sqlite`)
* Both C# and F# idiomatic functions will be visible when those namespaces are `import`ed or `open`ed
* There is a `Field` constructor for creating field conditions (though look at [v4][]'s changes here as well)
[v4]: ./v4.md#op-type-removal "Upgrade from v3 to v4 &bull; BitBadger.Documents"

View File

@ -1,35 +0,0 @@
# Upgrade from v3 to v4
## The Quick Version
- Add `BitBadger.Documents.[Postgres|Sqlite].Compat` to your list of `using` (C#) or `open` (F#) statements. This namespace has deprecated versions of the methods/functions that were removed in v4. These generate warnings, rather than the "I don't know what this is" compiler errors.
- If your code referenced `Query.[Action].[ById|ByField|etc]`, the sides of the query on each side of the `WHERE` clause are now separate. A query to patch a document by its ID would go from `Query.Patch.ById(tableName)` to `Query.ById(Query.Patch(tableName))`. These functions may also require more parameters; keep reading for details on that.
- Custom queries had to be used when querying more than one field, or when the results in the database needed to be ordered. v4 provides solutions for both of these within the library itself.
## `ByField` to `ByFields` and PostgreSQL Numbers
All methods/functions that ended with `ByField` now end with `ByFields`, and take a `FieldMatch` case (`Any` equates to `OR`, `All` equates to `AND`) and sequence of `Field` objects. These `Field`s need to have their values as well, because the PostgreSQL library will now cast the field from the document to numeric and bind the parameter as-is.
That is an action-packed paragraph; these changes have several ripple effects throughout the library:
- Queries like `Query.Find.ByField` would need the full collection of fields to generate the SQL. Instead, `Query.ByFields` takes a "first-half" statement as its first parameter, then the field match and parameters as its next two.
- `Field` instances in version 3 needed to have a parameter name, which was specified externally to the object itself. In version 4, `ParameterName` is an optional member of the `Field` object, and the library will generate parameter names if it is missing. In both C# and F#, the `.WithParameterName(string)` method can be chained to the `Field.[OP]` call to specify a name, and F# users can also use the language's `with` keyword (`{ Field.EQ "TheField" "value" with ParameterName = Some "@theField" }`).
## `Op` Type Removal
The `Op` type has been replaced with a `Comparison` type which captures both the type of comparison and the object of the comparison in one type. This is considered an internal implementation detail, as that type was not intended for use outside the library; however, it was `public`, so its removal warrants at least a mention.
Additionally, the addition of `In` and `InArray` field comparisons drove a change to the `Field` type's static creation functions. These now have the comparison spelled out, as opposed to the two-to-three character abbreviations. (These abbreviated functions still exists as aliases, so this change will not result in compile errors.) The functions to create fields are:
| Old | New |
|:-----:|-----------------------|
| `EQ` | `Equal` |
| `GT` | `Greater` |
| `GE` | `GreaterOrEqual` |
| `LT` | `Less` |
| `LE` | `LessOrEqual` |
| `NE` | `NotEqual` |
| `BT` | `Between` |
| `IN` | `In` _(since v4 rc1)_ |
| -- | `InArray` _(v4 rc4)_ |
| `EX` | `Exists` |
| `NEX` | `NotExists` |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -1,93 +0,0 @@
---
_layout: landing
title: Welcome!
---
BitBadger.Documents provides a lightweight document-style interface over [PostgreSQL][]'s and [SQLite][]'s JSON storage capabilities, with first-class support for both C# and F# programs. _(It is developed by the community; it is not officially affiliated with either project.)_
> [!TIP]
> Expecting `BitBadger.Npgsql.Documents`? This library replaced it as of v3.
## Installing
### PostgreSQL [![Nuget (with prereleases)][pkg-shield-pgsql]][pkg-link-pgsql]
```shell
dotnet add package BitBadger.Documents.Postgres
```
### SQLite [![Nuget (with prereleases)][pkg-shield-sqlite]][pkg-link-sqlite]
```shell
dotnet add package BitBadger.Documents.Sqlite
```
## Using
- **[Getting Started][]** provides an overview of the libraries' functions, how to provide connection details, and how to ensure required tables and indexes exist.
- **[Basic Usage][]** details document-level retrieval, persistence, and deletion.
- **[Advanced Usage][]** demonstrates how to use the building blocks provided by this library to write slightly-more complex queries.
## Upgrading Major Versions
* [v3 to v4][v3v4] ([Release][v4rel]) - Multiple field queries, ordering support, and automatic IDs
* [v2 to v3][v2v3] ([Release][v3rel]; upgrade from `BitBadger.Npgsql.Documents`) - Namespace / project change
* [v1 to v2][v1v2] ([Release][v2rel]) - Data storage format change
## Why Documents?
Document databases usually store <abbr title="JavaScript Object Notation">JSON</abbr> objects (as their "documents") to provide schemaless persistence of data; they also provide fault-tolerant ways to query that possibly-unstructured data. [MongoDB][] was the pioneer and is the leader in this space, but there are several who provide their own take on it, and their own programming <abbr title="Application Programming Interface">API</abbr> to come along with it. They also usually have some sort of clustering, replication, and sharding solution that allows them to be scaled out (horizontally) to handle a large amount of traffic.
As a mature relational database, PostgreSQL has a long history of robust data access from the .NET environment; Npgsql is actively developed, and provides both ADO.NET and <abbr title="Entity Framework">EF</abbr> Core APIs. PostgreSQL also has well-established, battle-tested horizontal scaling options. Additionally, the [Npgsql.FSharp][] project provides a functional API over Npgsql's ADO.NET data access. These three factors make PostgreSQL an excellent choice for document storage, and its relational nature can help in areas where traditional document databases become more complex.
SQLite is another mature relational database implemented as a single file, with its access run in-process with the calling application. It works very nicely on its own, with caching and write-ahead logging options; a companion project called [Litestream][] allows these files to be continuously streamed elsewhere, providing point-in-time recovery capabilities one would expect from a relational database. Microsoft provides ADO.NET (and EF Core) drivers for SQLite as part of .NET. These combine to make SQLite a compelling choice, and the hybrid relational/document model allows users to select the model of data that fits their model the best.
In both cases, the document access functions provided by this library are dead-simple. For more complex queries, it also provides the building blocks to construct these with minimal code.
## Why Not [something else]?
We are blessed to live in a time where there are a lot of good data storage options that are more than efficient enough for the majority of use cases. Rather than speaking ill of other projects, here is the vision of the benefits these libraries aim to provide:
### PostgreSQL
PostgreSQL is the most popular non-WordPress database for good reason.
- **Quality** - PostgreSQL's reputation is one of a rock-solid, well-maintained, and continually evolving database.
- **Availability** - Nearly every cloud database provider offers PostgreSQL, and for custom servers, it is a package install away from being up and running.
- **Efficiency** - PostgreSQL is very efficient, and its indexing of JSONB allows for quick access via any field in a document.
- **Maintainability** - The terms "separation of concerns" and "locality of behavior" often compete within a code base, and separation of concerns often wins out; cluttering your logic with SQL can be less than optimal. Using this library, though, it may separate the concerns enough that the calls can be placed directly in the regular logic, providing one fewer place that must be looked up when tracing through the code.
- **Simplicity** - <abbr title="Structured Query Language">SQL</abbr> is a familiar language; even when writing manual queries against the data store created by this library, everything one knows about SQL applies, with [a few operators added][json-ops].
- **Reliability** - The library has a full suite of tests against both the C# and F# APIs, [run against every supported PostgreSQL version][tests] to ensure the functionality provided is what is advertised.
### SQLite
The [SQLite "About" page][sqlite-about] has a short description of the project and its strengths. Simplicity, flexibility, and a large install base speak for themselves. A lot of people believe they will need a lot of features offered by server-based relational databases, and live with that complexity even when the project is small. A smarter move may be to build with SQLite; if the need arises for something more, the project is very likely a success!
Many of the benefits listed for PostgreSQL apply here as well, including its test coverage, but SQLite removes the requirement to run it as a server!
## Support
Issues can be filed on the project's GitHub repository.
[PostgreSQL]: https://www.postgresql.org/ "PostgreSQL"
[SQLite]: https://sqlite.org/ "SQLite"
[pkg-shield-pgsql]: https://img.shields.io/nuget/vpre/BitBadger.Documents.Postgres
[pkg-link-pgsql]: https://www.nuget.org/packages/BitBadger.Documents.Postgres/ "BitBadger.Documents.Postgres • NuGet"
[pkg-shield-sqlite]: https://img.shields.io/nuget/vpre/BitBadger.Documents.Sqlite
[pkg-link-sqlite]: https://www.nuget.org/packages/BitBadger.Documents.Sqlite/ "BitBadger.Documents.Sqlite • NuGet"
[Getting Started]: ./docs/getting-started.md "Getting Started • BitBadger.Documents"
[Basic Usage]: ./docs/basic-usage.md "Basic Usage • BitBadger.Documents"
[Advanced Usage]: ./docs/advanced/index.md "Advanced Usage • BitBadger.Documents"
[v3v4]: ./docs/upgrade/v4.md "Upgrade from v3 to v4 • BitBadger.Documents"
[v4rel]: https://git.bitbadger.solutions/bit-badger/BitBadger.Documents/releases/tag/v4 "Version 4 • Releases • BitBadger.Documents • Bit Badger Solutions Git"
[v2v3]: ./docs/upgrade/v3.md "Upgrade from v2 to v3 • BitBadger.Documents"
[v3rel]: https://git.bitbadger.solutions/bit-badger/BitBadger.Documents/releases/tag/v3 "Version 3 • Releases • BitBadger.Documents • Bit Badger Solutions Git"
[v1v2]: ./docs/upgrade/v2.md "Upgrade from v1 to v2 • BitBadger.Documents"
[v2rel]: https://github.com/bit-badger/BitBadger.Npgsql.Documents/releases/tag/v2 "Version 2 • Releases • BitBadger.Npgsql.Documents • GitHub"
[MongoDB]: https://www.mongodb.com/ "MongoDB"
[Npgsql.FSharp]: https://zaid-ajaj.github.io/Npgsql.FSharp/#/ "Npgsql.FSharp"
[Litestream]: https://litestream.io/ "Litestream"
[sqlite-about]: https://sqlite.org/about.html "About • SQLite"
[json-ops]: https://www.postgresql.org/docs/15/functions-json.html#FUNCTIONS-JSON-OP-TABLE "JSON Functions and Operators • Documentation • PostgreSQL"
[tests]: https://git.bitbadger.solutions/bit-badger/BitBadger.Documents/releases "Releases • BitBadger.Documents • Bit Badger Solutions Git"

View File

@ -2,8 +2,8 @@
<PropertyGroup>
<Description>Common files for PostgreSQL and SQLite document database libraries</Description>
<PackageReleaseNotes>v3 release</PackageReleaseNotes>
<PackageTags>JSON Document SQL</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@ -14,9 +14,7 @@
<ItemGroup>
<PackageReference Include="FSharp.SystemTextJson" Version="1.3.13" />
<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" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

View File

@ -1,402 +1,100 @@
namespace BitBadger.Documents
open System.Security.Cryptography
open System.Text
/// The types of logical operations available for JSON fields
[<Struct>]
type Op =
/// Equals (=)
| EQ
/// Greater Than (>)
| GT
/// Greater Than or Equal To (>=)
| GE
/// Less Than (<)
| LT
/// Less Than or Equal To (<=)
| LE
/// Not Equal to (<>)
| NE
/// Between (BETWEEN)
| BT
/// Exists (IS NOT NULL)
| EX
/// Does Not Exist (IS NULL)
| NEX
/// <summary>The types of comparisons available for JSON fields</summary>
/// <exclude />
type Comparison =
/// <summary>Equals (<c>=</c>)</summary>
| Equal of Value: obj
/// <summary>Greater Than (<c>&gt;</c>)</summary>
| Greater of Value: obj
/// <summary>Greater Than or Equal To (<c>&gt;=</c>)</summary>
| GreaterOrEqual of Value: obj
/// <summary>Less Than (<c>&lt;</c>)</summary>
| Less of Value: obj
/// <summary>Less Than or Equal To (<c>&lt;=</c>)</summary>
| LessOrEqual of Value: obj
/// <summary>Not Equal to (<c>&lt;&gt;</c>)</summary>
| NotEqual of Value: obj
/// <summary>Between (<c>BETWEEN</c>)</summary>
| Between of Min: obj * Max: obj
/// <summary>In (<c>IN</c>)</summary>
| In of Values: obj seq
/// <summary>In Array (PostgreSQL: <c>|?</c>, SQLite: <c>EXISTS / json_each / IN</c>)</summary>
| InArray of Table: string * Values: obj seq
/// <summary>Exists (<c>IS NOT NULL</c>)</summary>
| Exists
/// <summary>Does Not Exist (<c>IS NULL</c>)</summary>
| NotExists
/// <summary>The operator SQL for this comparison</summary>
member this.OpSql =
override this.ToString() =
match this with
| Equal _ -> "="
| Greater _ -> ">"
| GreaterOrEqual _ -> ">="
| Less _ -> "<"
| LessOrEqual _ -> "<="
| NotEqual _ -> "<>"
| Between _ -> "BETWEEN"
| In _ -> "IN"
| InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
| Exists -> "IS NOT NULL"
| NotExists -> "IS NULL"
| EQ -> "="
| GT -> ">"
| GE -> ">="
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
/// <summary>The dialect in which a command should be rendered</summary>
[<Struct>]
type Dialect =
| PostgreSQL
| SQLite
/// <summary>The format in which an element of a JSON field should be extracted</summary>
[<Struct>]
type FieldFormat =
/// <summary>
/// Use <c>-&gt;&gt;</c> or <c>#&gt;&gt;</c>; extracts a text (PostgreSQL) or SQL (SQLite) value
/// </summary>
| AsSql
/// <summary>Use <c>-&gt;</c> or <c>#&gt;</c>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value</summary>
| AsJson
/// <summary>Criteria for a field <c>WHERE</c> clause</summary>
/// Criteria for a field WHERE clause
type Field = {
/// <summary>The name of the field</summary>
/// The name of the field
Name: string
/// <summary>The comparison for the field</summary>
Comparison: Comparison
/// The operation by which the field will be compared
Op: Op
/// <summary>The name of the parameter for this field</summary>
ParameterName: string option
/// <summary>The table qualifier for this field</summary>
Qualifier: string option
/// The value of the field
Value: obj
} with
/// <summary>Create a comparison against a field</summary>
/// <param name="name">The name of the field against which the comparison should be applied</param>
/// <param name="comparison">The comparison for the given field</param>
/// <returns>A new <c>Field</c> instance implementing the given comparison</returns>
static member Where name (comparison: Comparison) =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
/// Create an equals (=) field criterion
static member EQ name (value: obj) =
{ Name = name; Op = EQ; Value = value }
/// <summary>Create an equals (<c>=</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Equal<'T> name (value: 'T) =
Field.Where name (Equal value)
/// Create a greater than (>) field criterion
static member GT name (value: obj) =
{ Name = name; Op = GT; Value = value }
/// <summary>Create an equals (<c>=</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member EQ<'T> name (value: 'T) = Field.Equal name value
/// Create a greater than or equal to (>=) field criterion
static member GE name (value: obj) =
{ Name = name; Op = GE; Value = value }
/// <summary>Create a greater than (<c>&gt;</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Greater<'T> name (value: 'T) =
Field.Where name (Greater value)
/// Create a less than (<) field criterion
static member LT name (value: obj) =
{ Name = name; Op = LT; Value = value }
/// <summary>Create a greater than (<c>&gt;</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GT<'T> name (value: 'T) = Field.Greater name value
/// Create a less than or equal to (<=) field criterion
static member LE name (value: obj) =
{ Name = name; Op = LE; Value = value }
/// <summary>Create a greater than or equal to (<c>&gt;=</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GreaterOrEqual<'T> name (value: 'T) =
Field.Where name (GreaterOrEqual value)
/// Create a not equals (<>) field criterion
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value }
/// <summary>Create a greater than or equal to (<c>&gt;=</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member GE<'T> name (value: 'T) = Field.GreaterOrEqual name value
/// Create a BETWEEN field criterion
static member BT name (min: obj) (max: obj) =
{ Name = name; Op = BT; Value = [ min; max ] }
/// <summary>Create a less than (<c>&lt;</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member Less<'T> name (value: 'T) =
Field.Where name (Less value)
/// Create an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj () }
/// <summary>Create a less than (<c>&lt;</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LT<'T> name (value: 'T) = Field.Less name value
/// <summary>Create a less than or equal to (<c>&lt;=</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LessOrEqual<'T> name (value: 'T) =
Field.Where name (LessOrEqual value)
/// <summary>Create a less than or equal to (<c>&lt;=</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member LE<'T> name (value: 'T) = Field.LessOrEqual name value
/// <summary>Create a not equals (<c>&lt;&gt;</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member NotEqual<'T> name (value: 'T) =
Field.Where name (NotEqual value)
/// <summary>Create a not equals (<c>&lt;&gt;</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="value">The value for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member NE<'T> name (value: 'T) = Field.NotEqual name value
/// <summary>Create a Between field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="min">The minimum value for the comparison range</param>
/// <param name="max">The maximum value for the comparison range</param>
/// <returns>A field with the given comparison</returns>
static member Between<'T> name (min: 'T) (max: 'T) =
Field.Where name (Between(min, max))
/// <summary>Create a Between field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="min">The minimum value for the comparison range</param>
/// <param name="max">The maximum value for the comparison range</param>
/// <returns>A field with the given comparison</returns>
static member BT<'T> name (min: 'T) (max: 'T) = Field.Between name min max
/// <summary>Create an In field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member In<'T> name (values: 'T seq) =
Field.Where name (In (Seq.map box values))
/// <summary>Create an In field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member IN<'T> name (values: 'T seq) = Field.In name values
/// <summary>Create an InArray field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <param name="tableName">The name of the table in which the field's documents are stored</param>
/// <param name="values">The values for the comparison</param>
/// <returns>A field with the given comparison</returns>
static member InArray<'T> name tableName (values: 'T seq) =
Field.Where name (InArray(tableName, Seq.map box values))
/// <summary>Create an exists (<c>IS NOT NULL</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member Exists name =
Field.Where name Exists
/// <summary>Create an exists (<c>IS NOT NULL</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member EX name = Field.Exists name
/// <summary>Create a not exists (<c>IS NULL</c>) field criterion</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member NotExists name =
Field.Where name NotExists
/// <summary>Create a not exists (<c>IS NULL</c>) field criterion (alias)</summary>
/// <param name="name">The name of the field to be compared</param>
/// <returns>A field with the given comparison</returns>
static member NEX name = Field.NotExists name
/// <summary>Transform a field name (<c>a.b.c</c>) to a path for the given SQL dialect</summary>
/// <param name="name">The name of the field in dotted format</param>
/// <param name="dialect">The SQL dialect to use when converting the name to nested path format</param>
/// <param name="format">Whether to reference this path as a JSON value or a SQL value</param>
/// <returns>A <c>string</c> with the path required to address the nested document value</returns>
static member NameToPath (name: string) dialect format =
let path =
if name.Contains '.' then
match dialect with
| PostgreSQL ->
(match format with AsJson -> "#>" | AsSql -> "#>>")
+ "'{" + String.concat "," (name.Split '.') + "}'"
| SQLite ->
let parts = name.Split '.'
let last = Array.last parts
let final = (match format with AsJson -> "'->'" | AsSql -> "'->>'") + $"{last}'"
"->'" + String.concat "'->'" (Array.truncate (Array.length parts - 1) parts) + final
else
match format with AsJson -> $"->'{name}'" | AsSql -> $"->>'{name}'"
$"data{path}"
/// <summary>Create a field with a given name, but no other properties filled</summary>
/// <param name="name">The field name, along with any other qualifications if used in a sorting context</param>
/// <remarks><c>Comparison</c> will be <c>Equal</c>, value will be an empty string</remarks>
static member Named name =
Field.Where name (Equal "")
/// <summary>Specify the name of the parameter for this field</summary>
/// <param name="name">The parameter name (including <c>:</c> or <c>@</c>)</param>
/// <returns>A field with the given parameter name specified</returns>
member this.WithParameterName name =
{ this with ParameterName = Some name }
/// <summary>Specify a qualifier (alias) for the table from which this field will be referenced</summary>
/// <param name="alias">The table alias for this field comparison</param>
/// <returns>A field with the given qualifier specified</returns>
member this.WithQualifier alias =
{ this with Qualifier = Some alias }
/// <summary>Get the qualified path to the field</summary>
/// <param name="dialect">The SQL dialect to use when converting the name to nested path format</param>
/// <param name="format">Whether to reference this path as a JSON value or a SQL value</param>
/// <returns>A <c>string</c> with the qualified path required to address the nested document value</returns>
member this.Path dialect format =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "")
+ Field.NameToPath this.Name dialect format
/// Create a not exists (IS NULL) field criterion
static member NEX name =
{ Name = name; Op = NEX; Value = obj () }
/// <summary>How fields should be matched</summary>
[<Struct>]
type FieldMatch =
/// <summary>Any field matches (<c>OR</c>)</summary>
| Any
/// <summary>All fields match (<c>AND</c>)</summary>
| All
/// <summary>The SQL value implementing each matching strategy</summary>
override this.ToString() =
match this with Any -> "OR" | All -> "AND"
/// <summary>Derive parameter names (each instance wraps a counter to uniquely name anonymous fields)</summary>
type ParameterName() =
/// The counter for the next field value
let mutable currentIdx = -1
/// <summary>
/// Return the specified name for the parameter, or an anonymous parameter name if none is specified
/// </summary>
/// <param name="paramName">The optional name of the parameter</param>
/// <returns>The name of the parameter, derived if no name was provided</returns>
member this.Derive paramName =
match paramName with
| Some it -> it
| None ->
currentIdx <- currentIdx + 1
$"@field{currentIdx}"
/// <summary>Automatically-generated document ID strategies</summary>
[<Struct>]
type AutoId =
/// <summary>No automatic IDs will be generated</summary>
| Disabled
/// <summary>Generate a MAX-plus-1 numeric value for documents</summary>
| Number
/// <summary>Generate a <c>GUID</c> for each document (as a lowercase, no-dashes, 32-character string)</summary>
| Guid
/// <summary>Generate a random string of hexadecimal characters for each document</summary>
| RandomString
with
/// <summary>Generate a <c>GUID</c> string</summary>
/// <returns>A <c>GUID</c> string</returns>
static member GenerateGuid() =
System.Guid.NewGuid().ToString "N"
/// <summary>Generate a string of random hexadecimal characters</summary>
/// <param name="length">The number of characters to generate</param>
/// <returns>A string of the given length with random hexadecimal characters</returns>
static member GenerateRandomString(length: int) =
RandomNumberGenerator.GetHexString(length, lowercase = true)
/// <summary>Does the given document need an automatic ID generated?</summary>
/// <param name="strategy">The auto-ID strategy currently in use</param>
/// <param name="document">The document being inserted</param>
/// <param name="idProp">The name of the ID property in the given document</param>
/// <returns>True if an auto-ID strategy is implemented and the ID has no value, false otherwise</returns>
/// <exception cref="T:System.InvalidOperationException">
/// If the ID field type and requested ID value are not compatible
/// </exception>
static member NeedsAutoId<'T> strategy (document: 'T) idProp =
match strategy with
| Disabled -> false
| _ ->
let prop = document.GetType().GetProperty idProp
if isNull prop then invalidOp $"{idProp} not found in document"
else
match strategy with
| Number ->
if prop.PropertyType = typeof<int8> then
let value = prop.GetValue document :?> int8
value = int8 0
elif prop.PropertyType = typeof<int16> then
let value = prop.GetValue document :?> int16
value = int16 0
elif prop.PropertyType = typeof<int> then
let value = prop.GetValue document :?> int
value = 0
elif prop.PropertyType = typeof<int64> then
let value = prop.GetValue document :?> int64
value = int64 0
else invalidOp "Document ID was not a number; cannot auto-generate a Number ID"
| Guid | RandomString ->
if prop.PropertyType = typeof<string> then
let value =
prop.GetValue document
|> Option.ofObj
|> Option.map (fun it -> it :?> string)
|> Option.defaultValue ""
value = ""
else invalidOp "Document ID was not a string; cannot auto-generate GUID or random string"
| Disabled -> false
/// <summary>The required document serialization implementation</summary>
/// The required document serialization implementation
type IDocumentSerializer =
/// <summary>Serialize an object to a JSON string</summary>
/// Serialize an object to a JSON string
abstract Serialize<'T> : 'T -> string
/// <summary>Deserialize a JSON string into an object</summary>
/// Deserialize a JSON string into an object
abstract Deserialize<'T> : string -> 'T
/// <summary>Document serializer defaults</summary>
/// Document serializer defaults
module DocumentSerializer =
open System.Text.Json
@ -408,7 +106,7 @@ module DocumentSerializer =
o.Converters.Add(JsonFSharpConverter())
o
/// <summary>The default JSON serializer</summary>
/// The default JSON serializer
[<CompiledName "Default">]
let ``default`` =
{ new IDocumentSerializer with
@ -419,90 +117,50 @@ module DocumentSerializer =
}
/// <summary>Configuration for document handling</summary>
/// Configuration for document handling
[<RequireQualifiedAccess>]
module Configuration =
/// The serializer to use for document manipulation
let mutable private serializerValue = DocumentSerializer.``default``
/// <summary>Register a serializer to use for translating documents to domain types</summary>
/// <param name="ser">The serializer to use when manipulating documents</param>
/// Register a serializer to use for translating documents to domain types
[<CompiledName "UseSerializer">]
let useSerializer ser =
serializerValue <- ser
/// <summary>Retrieve the currently configured serializer</summary>
/// <returns>The currently configured serializer</returns>
/// Retrieve the currently configured serializer
[<CompiledName "Serializer">]
let serializer () =
serializerValue
/// The serialized name of the ID field for documents
let mutable private idFieldValue = "Id"
let mutable idFieldValue = "Id"
/// <summary>Specify the name of the ID field for documents</summary>
/// <param name="it">The name of the ID field for documents</param>
/// Specify the name of the ID field for documents
[<CompiledName "UseIdField">]
let useIdField it =
idFieldValue <- it
/// <summary>Retrieve the currently configured ID field for documents</summary>
/// <returns>The currently configured ID field</returns>
/// Retrieve the currently configured ID field for documents
[<CompiledName "IdField">]
let idField () =
idFieldValue
/// The automatic ID strategy used by the library
let mutable private autoIdValue = Disabled
/// <summary>Specify the automatic ID generation strategy used by the library</summary>
/// <param name="it">The automatic ID generation strategy to use</param>
[<CompiledName "UseAutoIdStrategy">]
let useAutoIdStrategy it =
autoIdValue <- it
/// <summary>Retrieve the currently configured automatic ID generation strategy</summary>
/// <returns>The current automatic ID generation strategy</returns>
[<CompiledName "AutoIdStrategy">]
let autoIdStrategy () =
autoIdValue
/// The length of automatically generated random strings
let mutable private idStringLengthValue = 16
/// <summary>Specify the length of automatically generated random strings</summary>
/// <param name="length">The length of automatically generated random strings</param>
[<CompiledName "UseIdStringLength">]
let useIdStringLength length =
idStringLengthValue <- length
/// <summary>Retrieve the currently configured length of automatically generated random strings</summary>
/// <returns>The current length of automatically generated random strings</returns>
[<CompiledName "IdStringLength">]
let idStringLength () =
idStringLengthValue
/// <summary>Query construction functions</summary>
/// Query construction functions
[<RequireQualifiedAccess>]
module Query =
/// <summary>Combine a query (<c>SELECT</c>, <c>UPDATE</c>, etc.) and a <c>WHERE</c> clause</summary>
/// <param name="statement">The first part of the statement</param>
/// <param name="where">The <c>WHERE</c> clause for the statement</param>
/// <returns>The two parts of the query combined with <c>WHERE</c></returns>
[<CompiledName "StatementWhere">]
let statementWhere statement where =
$"%s{statement} WHERE %s{where}"
/// Create a SELECT clause to retrieve the document data from the given table
[<CompiledName "SelectFromTable">]
let selectFromTable tableName =
$"SELECT data FROM %s{tableName}"
/// <summary>Queries to define tables and indexes</summary>
/// Queries to define tables and indexes
module Definition =
/// <summary>SQL statement to create a document table</summary>
/// <param name="name">The name of the table to create (may include schema)</param>
/// <param name="dataType">The type of data for the column (<c>JSON</c>, <c>JSONB</c>, etc.)</param>
/// <returns>A query to create a document table</returns>
/// SQL statement to create a document table
[<CompiledName "EnsureTableFor">]
let ensureTableFor name dataType =
$"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)"
@ -512,14 +170,9 @@ module Query =
let parts = tableName.Split '.'
if Array.length parts = 1 then "", tableName else parts[0], parts[1]
/// <summary>SQL statement to create an index on one or more fields in a JSON document</summary>
/// <param name="tableName">The table on which an index should be created (may include schema)</param>
/// <param name="indexName">The name of the index to be created</param>
/// <param name="fields">One or more fields to include in the index</param>
/// <param name="dialect">The SQL dialect to use when creating this index</param>
/// <returns>A query to create the field index</returns>
/// SQL statement to create an index on one or more fields in a JSON document
[<CompiledName "EnsureIndexOn">]
let ensureIndexOn tableName indexName (fields: string seq) dialect =
let ensureIndexOn tableName indexName (fields: string seq) =
let _, tbl = splitSchemaAndTable tableName
let jsonFields =
fields
@ -527,154 +180,24 @@ module Query =
let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"({Field.NameToPath fieldName dialect AsSql}){direction}")
$"(data ->> '{fieldName}'){direction}")
|> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
/// <summary>SQL statement to create a key index for a document table</summary>
/// <param name="tableName">The table on which a key index should be created (may include schema)</param>
/// <param name="dialect">The SQL dialect to use when creating this index</param>
/// <returns>A query to create the key index</returns>
/// SQL statement to create a key index for a document table
[<CompiledName "EnsureKey">]
let ensureKey tableName dialect =
(ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX")
let ensureKey tableName =
(ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX")
/// <summary>Query to insert a document</summary>
/// <param name="tableName">The table into which to insert (may include schema)</param>
/// <returns>A query to insert a document</returns>
/// Query to insert a document
[<CompiledName "Insert">]
let insert tableName =
$"INSERT INTO %s{tableName} VALUES (@data)"
/// <summary>
/// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
/// </summary>
/// <param name="tableName">The table into which to save (may include schema)</param>
/// <returns>A query to save a document</returns>
[<CompiledName "Save">]
let save tableName =
sprintf
"INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ())
/// <summary>Query to count documents in a table</summary>
/// <param name="tableName">The table in which to count documents (may include schema)</param>
/// <returns>A query to count documents</returns>
/// <remarks>This query has no <c>WHERE</c> clause</remarks>
[<CompiledName "Count">]
let count tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// <summary>Query to check for document existence in a table</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="where">The <c>WHERE</c> clause with the existence criteria</param>
/// <returns>A query to check document existence</returns>
[<CompiledName "Exists">]
let exists tableName where =
$"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it"
/// <summary>Query to select documents from a table</summary>
/// <param name="tableName">The table from which documents should be found (may include schema)</param>
/// <returns>A query to retrieve documents</returns>
/// <remarks>This query has no <c>WHERE</c> clause</remarks>
[<CompiledName "Find">]
let find tableName =
$"SELECT data FROM %s{tableName}"
/// <summary>Query to update (replace) a document</summary>
/// <param name="tableName">The table in which documents should be replaced (may include schema)</param>
/// <returns>A query to update documents</returns>
/// <remarks>This query has no <c>WHERE</c> clause</remarks>
[<CompiledName "Update">]
let update tableName =
$"UPDATE %s{tableName} SET data = @data"
/// <summary>Query to delete documents from a table</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <returns>A query to delete documents</returns>
/// <remarks>This query has no <c>WHERE</c> clause</remarks>
[<CompiledName "Delete">]
let delete tableName =
$"DELETE FROM %s{tableName}"
/// <summary>Create a SELECT clause to retrieve the document data from the given table</summary>
/// <param name="tableName">The table from which documents should be found (may include schema)</param>
/// <returns>A query to retrieve documents</returns>
[<CompiledName "SelectFromTable">]
[<System.Obsolete "Use Find instead">]
let selectFromTable tableName =
find tableName
/// <summary>Create an <c>ORDER BY</c> clause for the given fields</summary>
/// <param name="fields">One or more fields by which to order</param>
/// <param name="dialect">The SQL dialect for the generated clause</param>
/// <returns>An <c>ORDER BY</c> clause for the given fields</returns>
[<CompiledName "OrderBy">]
let orderBy fields dialect =
if Seq.isEmpty fields then ""
else
fields
|> Seq.map (fun it ->
if it.Name.Contains ' ' then
let parts = it.Name.Split ' '
{ it with Name = parts[0] }, Some $""" {parts |> Array.skip 1 |> String.concat " "}"""
else it, None)
|> Seq.map (fun (field, direction) ->
if field.Name.StartsWith "n:" then
let f = { field with Name = field.Name[2..] }
match dialect with
| PostgreSQL -> $"({f.Path PostgreSQL AsSql})::numeric"
| SQLite -> f.Path SQLite AsSql
elif field.Name.StartsWith "i:" then
let p = { field with Name = field.Name[2..] }.Path dialect AsSql
match dialect with PostgreSQL -> $"LOWER({p})" | SQLite -> $"{p} COLLATE NOCASE"
else field.Path dialect AsSql
|> function path -> path + defaultArg direction "")
|> String.concat ", "
|> function it -> $" ORDER BY {it}"
#nowarn "FS3511" // "let rec" is not statically compilable
open System.IO.Pipelines
/// <summary>Functions that manipulate <c>PipeWriter</c>s</summary>
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module PipeWriter =
/// <summary>Write a UTF-8 string to this pipe</summary>
/// <param name="writer">The PipeWriter to which the string should be written</param>
/// <param name="text">The string to be written to the pipe</param>
/// <returns><c>true</c> if the pipe is still open, <c>false</c> if not</returns>
[<CompiledName "WriteString">]
let writeString (writer: PipeWriter) (text: string) = backgroundTask {
try
let! writeResult = writer.WriteAsync(Encoding.UTF8.GetBytes text)
return not writeResult.IsCompleted
with :? System.ObjectDisposedException -> return false
}
/// <summary>Write an array of strings, abandoning the sequence if the pipe is closed</summary>
/// <param name="writer">The PipeWriter to which the strings should be written</param>
/// <param name="items">The strings to be written</param>
/// <returns><c>true</c> if the pipe is still open, <c>false</c> if not</returns>
[<CompiledName "WriteStrings">]
let writeStrings writer items = backgroundTask {
let theItems = Seq.cache items
let rec writeNext idx = backgroundTask {
match theItems |> Seq.tryItem idx with
| Some item ->
if idx > 0 then
let! _ = writeString writer ","
()
match! writeString writer item with
| true -> return! writeNext (idx + 1)
| false -> return false
| None -> return true
}
let! _ = writeString writer "["
let! isCleanFinish = writeNext 0
if isCleanFinish then
let! _ = writeString writer "]"
()
}

View File

@ -7,12 +7,11 @@ This package provides common definitions and functionality for `BitBadger.Docume
## Features
- 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 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)
- Access documents as your domain models (<abbr title="Plain Old CLR Objects">POCO</abbr>s), as JSON strings, or as JSON written directly to a `PipeWriter`
- Use `Task`-based async for all data access functions
- Use building blocks for more complex 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)
- Uses `Task`-based async for all data access functions
- Uses building blocks for more complex queries
## Getting Started
Install the library of your choice and follow its README; also, the [project site](https://relationaldocs.bitbadger.solutions/dotnet/) has complete documentation.
Install the library of your choice and follow its README; also, the [project site](https://bitbadger.solutions/open-source/relational-documents/) has complete documentation.

View File

@ -1,17 +1,17 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyVersion>4.1.0.0</AssemblyVersion>
<FileVersion>4.1.0.0</FileVersion>
<VersionPrefix>4.1.0</VersionPrefix>
<PackageReleaseNotes>Add JSON retrieval and pipe-writing functions; update project URL to site with public API docs</PackageReleaseNotes>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<FileVersion>3.1.0.0</FileVersion>
<VersionPrefix>3.1.0</VersionPrefix>
<PackageReleaseNotes>Add BT (between) operator; drop .NET 7 support</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://relationaldocs.bitbadger.solutions/dotnet/</PackageProjectUrl>
<PackageProjectUrl>https://bitbadger.solutions/open-source/relational-documents/</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://git.bitbadger.solutions/bit-badger/BitBadger.Documents</RepositoryUrl>
<RepositoryType>Git</RepositoryType>

View File

@ -2,24 +2,20 @@
<PropertyGroup>
<Description>Use PostgreSQL as a document database</Description>
<PackageReleaseNotes>v3 release; official replacement for BitBadger.Npgsql.Documents</PackageReleaseNotes>
<PackageTags>JSON Document PostgreSQL Npgsql</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<Compile Include="WithProps.fs" />
<Compile Include="Functions.fs" />
<Compile Include="Extensions.fs" />
<Compile Include="Compat.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="9.0.2" />
<PackageReference Include="Npgsql.FSharp" Version="8.0.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@ -1,270 +0,0 @@
namespace BitBadger.Documents.Postgres.Compat
open BitBadger.Documents
open BitBadger.Documents.Postgres
[<AutoOpen>]
module Parameters =
/// Create a JSON field parameter
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams (F#) / AddFields (C#) instead ~ will be removed in v4.1">]
let addFieldParam name field parameters =
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FieldName">]
[<System.Obsolete "Use fieldNameParams (F#) / FieldNames (C#) instead ~ will be removed in v4.1">]
let fieldNameParam fieldNames =
fieldNameParams fieldNames
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
[<System.Obsolete "Use WhereByFields instead ~ will be removed in v4.1">]
let whereByField field paramName =
Query.whereByFields Any [ { field with ParameterName = Some paramName } ]
module WithProps =
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Count.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Exists.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field sqlProps =
WithProps.Find.byFields<'TDoc> tableName Any [ field ] sqlProps
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field, sqlProps) =
WithProps.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field sqlProps =
WithProps.Find.firstByFields<'TDoc> tableName Any [ field ] sqlProps
/// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found
[<System.Obsolete "Use FirstByFields instead ~ will be removed in v4.1">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, sqlProps) =
WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) sqlProps =
WithProps.Patch.byFields tableName Any [ field ] patch sqlProps
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames sqlProps =
WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames sqlProps
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field sqlProps =
WithProps.Delete.byFields tableName Any [ field ] sqlProps
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Count.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Exists.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field =
Find.byFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field) =
Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field =
Find.firstByFields<'TDoc> tableName Any [ field ]
/// 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">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) =
Patch.byFields tableName Any [ field ] patch
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames =
RemoveFields.byFields tableName Any [ field ] fieldNames
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Delete.byFields tableName Any [ field ]
open Npgsql
/// F# Extensions for the NpgsqlConnection type
[<AutoOpen>]
module Extensions =
type NpgsqlConnection with
/// Count matching documents using a JSON field comparison query (->> =)
[<System.Obsolete "Use countByFields instead ~ will be removed in v4.1">]
member conn.countByField tableName field =
conn.countByFields tableName Any [ field ]
/// Determine if documents exist using a JSON field comparison query (->> =)
[<System.Obsolete "Use existsByFields instead ~ will be removed in v4.1">]
member conn.existsByField tableName field =
conn.existsByFields tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use findByFields instead ~ will be removed in v4.1">]
member conn.findByField<'TDoc> tableName field =
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<System.Obsolete "Use findFirstByFields instead ~ will be removed in v4.1">]
member conn.findFirstByField<'TDoc> tableName field =
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<System.Obsolete "Use patchByFields instead ~ will be removed in v4.1">]
member conn.patchByField tableName field (patch: 'TPatch) =
conn.patchByFields tableName Any [ field ] patch
/// Remove fields from documents via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead ~ will be removed in v4.1">]
member conn.removeFieldsByField tableName field fieldNames =
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// Delete documents by matching a JSON field comparison query (->> =)
[<System.Obsolete "Use deleteByFields instead ~ will be removed in v4.1">]
member conn.deleteByField tableName field =
conn.deleteByFields tableName Any [ field ]
open System.Runtime.CompilerServices
open Npgsql.FSharp
type NpgsqlConnectionCSharpCompatExtensions =
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use CountByFields instead ~ will be removed in v4.1">]
static member inline CountByField(conn, tableName, field) =
WithProps.Count.byFields tableName Any [ field ] (Sql.existingConnection conn)
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead ~ will be removed in v4.1">]
static member inline ExistsByField(conn, tableName, field) =
WithProps.Exists.byFields tableName Any [ field ] (Sql.existingConnection conn)
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use FindByFields instead ~ will be removed in v4.1">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithProps.Find.ByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
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)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
[<System.Obsolete "Use PatchByFields instead ~ will be removed in v4.1">]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) =
WithProps.Patch.byFields tableName Any [ field ] patch (Sql.existingConnection conn)
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead ~ will be removed in v4.1">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames (Sql.existingConnection conn)
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead ~ will be removed in v4.1">]
static member inline DeleteByField(conn, tableName, field) =
WithProps.Delete.byFields tableName Any [ field ] (Sql.existingConnection conn)

File diff suppressed because it is too large Load Diff

View File

@ -1,976 +0,0 @@
namespace BitBadger.Documents.Postgres
/// <summary>Commands to execute custom SQL queries</summary>
[<RequireQualifiedAccess>]
module Custom =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.list<'TDoc> query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>) =
WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, fromDataSource ())
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
[<CompiledName "FSharpJsonArray">]
let jsonArray query parameters mapFunc =
WithProps.Custom.jsonArray query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
let JsonArray(query, parameters, mapFunc) =
WithProps.Custom.JsonArray(query, parameters, mapFunc, fromDataSource ())
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
[<CompiledName "FSharpWriteJsonArray">]
let writeJsonArray query parameters writer mapFunc =
WithProps.Custom.writeJsonArray query parameters writer mapFunc (fromDataSource ())
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
let WriteJsonArray(query, parameters, writer, mapFunc) =
WithProps.Custom.WriteJsonArray(query, parameters, writer, mapFunc, fromDataSource ())
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns><c>Some</c> with the first matching result, or <c>None</c> if not found</returns>
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) =
WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>The first matching result, or <c>null</c> if not found</returns>
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 ())
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
[<CompiledName "FSharpJsonSingle">]
let jsonSingle query parameters mapFunc =
WithProps.Custom.jsonSingle query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
let JsonSingle(query, parameters, mapFunc) =
WithProps.Custom.JsonSingle(query, parameters, mapFunc, fromDataSource ())
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
[<CompiledName "NonQuery">]
let nonQuery query parameters =
WithProps.Custom.nonQuery query parameters (fromDataSource ())
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) =
WithProps.Custom.scalar query parameters mapFunc (fromDataSource ())
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>) =
WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ())
/// <summary>Table and index definition commands</summary>
[<RequireQualifiedAccess>]
module Definition =
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
[<CompiledName "EnsureTable">]
let ensureTable name =
WithProps.Definition.ensureTable name (fromDataSource ())
/// <summary>Create an index on documents in the specified table</summary>
/// <param name="name">The table to be indexed (may include schema)</param>
/// <param name="idxType">The type of document index to create</param>
[<CompiledName "EnsureDocumentIndex">]
let ensureDocumentIndex name idxType =
WithProps.Definition.ensureDocumentIndex name idxType (fromDataSource ())
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
WithProps.Definition.ensureFieldIndex tableName indexName fields (fromDataSource ())
/// <summary>Document writing functions</summary>
[<AutoOpen>]
module Document =
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.insert<'TDoc> tableName document (fromDataSource ())
/// <summary>Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")</summary>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) =
WithProps.Document.save<'TDoc> tableName document (fromDataSource ())
/// <summary>Queries to count documents</summary>
[<RequireQualifiedAccess>]
module Count =
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "All">]
let all tableName =
WithProps.Count.all tableName (fromDataSource ())
/// <summary>Count matching documents using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Count.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Count matching documents using a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Count.byContains tableName criteria (fromDataSource ())
/// <summary>Count matching documents using a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ())
/// <summary>Queries to determine if documents exist</summary>
[<RequireQualifiedAccess>]
module Exists =
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
[<CompiledName "ById">]
let byId tableName docId =
WithProps.Exists.byId tableName docId (fromDataSource ())
/// <summary>Determine if a document exists using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Exists.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Determine if a document exists using a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByContains">]
let byContains tableName criteria =
WithProps.Exists.byContains tableName criteria (fromDataSource ())
/// <summary>Determine if a document exists using a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to be matched</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ())
/// <summary>Commands to retrieve documents as domain objects</summary>
[<RequireQualifiedAccess>]
module Find =
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
WithProps.Find.all<'TDoc> tableName (fromDataSource ())
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
let All<'TDoc> tableName =
WithProps.Find.All<'TDoc>(tableName, fromDataSource ())
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields =
WithProps.Find.allOrdered<'TDoc> tableName orderFields (fromDataSource ())
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
let AllOrdered<'TDoc> tableName orderFields =
WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, fromDataSource ())
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns><c>Some</c> with the document if found, <c>None</c> otherwise</returns>
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ())
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The document if found, <c>null</c> otherwise</returns>
let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey) =
WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ())
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields =
WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
let ByFields<'TDoc>(tableName, howMatched, fields) =
WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "FSharpByFieldsOrdered">]
let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) =
WithProps.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ())
/// <summary>Retrieve documents matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>All documents matching the given containment query</returns>
[<CompiledName "FSharpByContains">]
let byContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.byContains<'TDoc> tableName criteria (fromDataSource ())
/// <summary>Retrieve documents matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>All documents matching the given containment query</returns>
let ByContains<'TDoc>(tableName, criteria: obj) =
WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
[<CompiledName "FSharpByContainsOrdered">]
let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields) =
WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ())
/// <summary>Retrieve documents matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>All documents matching the given JSON Path expression</returns>
[<CompiledName "FSharpByJsonPath">]
let byJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// <summary>Retrieve documents matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>All documents matching the given JSON Path expression</returns>
let ByJsonPath<'TDoc>(tableName, jsonPath) =
WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
[<CompiledName "FSharpByJsonPathOrdered">]
let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Retrieve documents matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given JSON Path expression, ordered by the given fields</returns>
let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields) =
WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ())
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) =
WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ())
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns><c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
WithProps.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields) =
WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByContains">]
let firstByContains<'TDoc> tableName (criteria: obj) =
WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ())
/// <summary>Retrieve the first document matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj) =
WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns><c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByContainsOrdered">]
let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields =
WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, orderFields) =
WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ())
/// <summary>Retrieve the first document matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByJsonPath">]
let firstByJsonPath<'TDoc> tableName jsonPath =
WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ())
/// <summary>Retrieve the first document matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath) =
WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns><c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByJsonPathOrdered">]
let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields =
WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Retrieve the first document matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, orderFields) =
WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ())
/// <summary>Commands to retrieve documents as JSON</summary>
[<RequireQualifiedAccess>]
module Json =
/// <summary>Retrieve all documents in the given table as a JSON array</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table as a JSON array</returns>
[<CompiledName "All">]
let all tableName =
WithProps.Json.all tableName (fromDataSource ())
/// <summary>Write all documents in the given table to the given <c>PipeWriter</c></summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
[<CompiledName "WriteAll">]
let writeAll tableName writer =
WithProps.Json.writeAll tableName writer (fromDataSource ())
/// <summary>
/// Retrieve all documents in the given table as a JSON array, ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table as a JSON array, ordered by the given fields</returns>
[<CompiledName "AllOrdered">]
let allOrdered tableName orderFields =
WithProps.Json.allOrdered tableName orderFields (fromDataSource ())
/// <summary>
/// Write all documents in the given table to the given <c>PipeWriter</c>, ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteAllOrdered">]
let writeAllOrdered tableName writer orderFields =
WithProps.Json.writeAllOrdered tableName writer orderFields (fromDataSource ())
/// <summary>Retrieve a JSON document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "ById">]
let byId<'TKey> tableName (docId: 'TKey) =
WithProps.Json.byId tableName docId (fromDataSource ())
/// <summary>Write a JSON document to the given <c>PipeWriter</c> by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="docId">The ID of the document to retrieve</param>
[<CompiledName "WriteById">]
let writeById<'TKey> tableName writer (docId: 'TKey) =
WithProps.Json.writeById tableName writer docId (fromDataSource ())
/// <summary>Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All JSON documents matching the given fields</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Json.byFields tableName howMatched fields (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "WriteByFields">]
let writeByFields tableName writer howMatched fields =
WithProps.Json.writeByFields tableName writer howMatched fields (fromDataSource ())
/// <summary>
/// Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields
/// in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "ByFieldsOrdered">]
let byFieldsOrdered tableName howMatched queryFields orderFields =
WithProps.Json.byFieldsOrdered tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteByFieldsOrdered">]
let writeByFieldsOrdered tableName writer howMatched queryFields orderFields =
WithProps.Json.writeByFieldsOrdered tableName writer howMatched queryFields orderFields (fromDataSource ())
/// <summary>Retrieve JSON documents matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>All JSON documents matching the given containment query</returns>
[<CompiledName "ByContains">]
let byContains tableName (criteria: obj) =
WithProps.Json.byContains tableName criteria (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching a JSON containment query (<c>@&gt;</c>)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="criteria">The document to match with the containment query</param>
[<CompiledName "WriteByContains">]
let writeByContains tableName writer (criteria: obj) =
WithProps.Json.writeByContains tableName writer criteria (fromDataSource ())
/// <summary>
/// Retrieve JSON documents matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given containment query, ordered by the given fields</returns>
[<CompiledName "ByContainsOrdered">]
let byContainsOrdered tableName (criteria: obj) orderFields =
WithProps.Json.byContainsOrdered tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching a JSON containment query (<c>@&gt;</c>) ordered by
/// the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteByContainsOrdered">]
let writeByContainsOrdered tableName writer (criteria: obj) orderFields =
WithProps.Json.writeByContainsOrdered tableName writer criteria orderFields (fromDataSource ())
/// <summary>Retrieve JSON documents matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>All JSON documents matching the given JSON Path expression</returns>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Json.byJsonPath tableName jsonPath (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching a JSON Path match query (<c>@?</c>)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
[<CompiledName "WriteByJsonPath">]
let writeByJsonPath tableName writer jsonPath =
WithProps.Json.writeByJsonPath tableName writer jsonPath (fromDataSource ())
/// <summary>
/// Retrieve JSON documents matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents matching the given JSON Path expression, ordered by the given fields</returns>
[<CompiledName "ByJsonPathOrdered">]
let byJsonPathOrdered tableName jsonPath orderFields =
WithProps.Json.byJsonPathOrdered tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching a JSON Path match query (<c>@?</c>) ordered by the
/// given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteByJsonPathOrdered">]
let writeByJsonPathOrdered tableName writer jsonPath orderFields =
WithProps.Json.writeByJsonPathOrdered tableName writer jsonPath orderFields (fromDataSource ())
/// <summary>Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFields">]
let firstByFields tableName howMatched fields =
WithProps.Json.firstByFields tableName howMatched fields (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "WriteFirstByFields">]
let writeFirstByFields tableName writer howMatched fields =
WithProps.Json.writeFirstByFields tableName writer howMatched fields (fromDataSource ())
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFieldsOrdered">]
let firstByFieldsOrdered tableName howMatched queryFields orderFields =
WithProps.Json.firstByFieldsOrdered tableName howMatched queryFields orderFields (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteFirstByFieldsOrdered">]
let writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields =
WithProps.Json.writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields (fromDataSource ())
/// <summary>Retrieve the first JSON document matching a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByContains">]
let firstByContains tableName (criteria: obj) =
WithProps.Json.firstByContains tableName criteria (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching a JSON containment query (<c>@&gt;</c>)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="criteria">The document to match with the containment query</param>
[<CompiledName "WriteFirstByContains">]
let writeFirstByContains tableName writer (criteria: obj) =
WithProps.Json.writeFirstByContains tableName writer criteria (fromDataSource ())
/// <summary>
/// Retrieve the first JSON document matching a JSON containment query (<c>@&gt;</c>) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByContainsOrdered">]
let firstByContainsOrdered tableName (criteria: obj) orderFields =
WithProps.Json.firstByContainsOrdered tableName criteria orderFields (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching a JSON containment query (<c>@&gt;</c>)
/// ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="criteria">The document to match with the containment query</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteFirstByContainsOrdered">]
let writeFirstByContainsOrdered tableName writer (criteria: obj) orderFields =
WithProps.Json.writeFirstByContainsOrdered tableName writer criteria orderFields (fromDataSource ())
/// <summary>Retrieve the first JSON document matching a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByJsonPath">]
let firstByJsonPath tableName jsonPath =
WithProps.Json.firstByJsonPath tableName jsonPath (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching a JSON Path match query (<c>@?</c>)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
[<CompiledName "WriteFirstByJsonPath">]
let writeFirstByJsonPath tableName writer jsonPath =
WithProps.Json.writeFirstByJsonPath tableName writer jsonPath (fromDataSource ())
/// <summary>
/// Retrieve the first JSON document matching a JSON Path match query (<c>@?</c>) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first matching JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByJsonPathOrdered">]
let firstByJsonPathOrdered tableName jsonPath orderFields =
WithProps.Json.firstByJsonPathOrdered tableName jsonPath orderFields (fromDataSource ())
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching a JSON Path match query (<c>@?</c>)
/// ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteFirstByJsonPathOrdered">]
let writeFirstByJsonPathOrdered tableName writer jsonPath orderFields =
WithProps.Json.writeFirstByJsonPathOrdered tableName writer jsonPath orderFields (fromDataSource ())
/// <summary>Commands to update documents</summary>
[<RequireQualifiedAccess>]
module Update =
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
WithProps.Update.byId tableName docId document (fromDataSource ())
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
[<CompiledName "FSharpFullFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
WithProps.Update.byFunc tableName idFunc document (fromDataSource ())
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
WithProps.Update.ByFunc(tableName, idFunc, document, fromDataSource ())
/// <summary>Commands to patch (partially update) documents</summary>
[<RequireQualifiedAccess>]
module Patch =
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
WithProps.Patch.byId tableName docId patch (fromDataSource ())
/// <summary>
/// Patch documents using a JSON field comparison query in the <c>WHERE</c> clause (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) =
WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ())
/// <summary>Patch documents using a JSON containment query in the <c>WHERE</c> clause (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) =
WithProps.Patch.byContains tableName criteria patch (fromDataSource ())
/// <summary>Patch documents using a JSON Path match query in the <c>WHERE</c> clause (<c>@?</c>)</summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath (patch: 'TPatch) =
WithProps.Patch.byJsonPath tableName jsonPath patch (fromDataSource ())
/// <summary>Commands to remove fields from documents</summary>
[<RequireQualifiedAccess>]
module RemoveFields =
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames =
WithProps.RemoveFields.byId tableName docId fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a JSON containment query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) fieldNames =
WithProps.RemoveFields.byContains tableName criteria fieldNames (fromDataSource ())
/// <summary>Remove fields from documents via a JSON Path match query (<c>@?</c>)</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath fieldNames =
WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (fromDataSource ())
/// <summary>Commands to delete documents</summary>
[<RequireQualifiedAccess>]
module Delete =
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
WithProps.Delete.byId tableName docId (fromDataSource ())
/// <summary>Delete documents by matching a JSON field comparison query (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
WithProps.Delete.byFields tableName howMatched fields (fromDataSource ())
/// <summary>Delete documents by matching a JSON contains query (<c>@&gt;</c>)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="criteria">The document to match the containment query</param>
[<CompiledName "ByContains">]
let byContains tableName (criteria: 'TContains) =
WithProps.Delete.byContains tableName criteria (fromDataSource ())
/// <summary>Delete documents by matching a JSON Path match query (@?)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="jsonPath">The JSON Path expression to match</param>
[<CompiledName "ByJsonPath">]
let byJsonPath tableName jsonPath =
WithProps.Delete.byJsonPath tableName jsonPath (fromDataSource ())

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,11 @@ This package provides a lightweight document library backed by [PostgreSQL](http
## Features
- 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)
- 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 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://relationaldocs.bitbadger.solutions/dotnet/upgrade/v4.html) for full details.
## Getting Started
Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library:
@ -71,7 +66,7 @@ var customer = await Find.ById<string, Customer>("customer", "123");
// Find.byId type signature is string -> 'TKey -> Task<'TDoc option>
let! customer = Find.byId<string, Customer> "customer" "123"
```
_(keys are treated as strings or numbers depending on their definition; however, they are indexed as strings)_
_(keys are treated as strings in the database)_
Count customers in Atlanta (using JSON containment):
@ -103,4 +98,4 @@ do! Delete.byJsonPath "customer" """$.City ? (@ == "Chicago")"""
## More Information
The [project site](https://relationaldocs.bitbadger.solutions/dotnet/) has full details on how to use this library.
The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library.

File diff suppressed because it is too large Load Diff

View File

@ -2,23 +2,20 @@
<PropertyGroup>
<Description>Use SQLite as a document database</Description>
<PackageReleaseNotes>Overall v3 release; initial release supporting SQLite</PackageReleaseNotes>
<PackageTags>JSON Document SQLite</PackageTags>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Library.fs" />
<Compile Include="WithConn.fs" />
<Compile Include="Functions.fs" />
<Compile Include="Extensions.fs" />
<Compile Include="Compat.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@ -1,269 +0,0 @@
namespace BitBadger.Documents.Sqlite.Compat
open BitBadger.Documents
open BitBadger.Documents.Sqlite
[<AutoOpen>]
module Parameters =
/// Create a JSON field parameter
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams (F#) / AddFields (C#) instead ~ will be removed in v4.1">]
let addFieldParam name field parameters =
addFieldParams [ { field with ParameterName = Some name } ] parameters
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FieldName">]
[<System.Obsolete "Use fieldNameParams (F#) / FieldNames (C#) instead ~ will be removed in v4.1">]
let fieldNameParam fieldNames =
fieldNameParams fieldNames
[<RequireQualifiedAccess>]
module Query =
/// Create a WHERE clause fragment to implement a comparison on a field in a JSON document
[<CompiledName "WhereByField">]
[<System.Obsolete "Use WhereByFields instead ~ will be removed in v4.1">]
let whereByField field paramName =
Query.whereByFields Any [ { field with ParameterName = Some paramName } ]
module WithConn =
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Count.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Exists.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field conn =
WithConn.Find.byFields<'TDoc> tableName Any [ field ] conn
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field, conn) =
WithConn.Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field conn =
WithConn.Find.firstByFields<'TDoc> tableName Any [ field ] conn
/// 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">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, conn) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) conn =
WithConn.Patch.byFields tableName Any [ field ] patch conn
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames conn =
WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field conn =
WithConn.Delete.byFields tableName Any [ field ] conn
[<RequireQualifiedAccess>]
module Count =
/// Count matching documents using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Count.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists using a JSON field comparison (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Exists.byFields tableName Any [ field ]
[<RequireQualifiedAccess>]
module Find =
/// Retrieve documents matching a JSON field comparison (->> =)
[<CompiledName "FSharpByField">]
[<System.Obsolete "Use byFields instead ~ will be removed in v4.1">]
let byField<'TDoc> tableName field =
Find.byFields<'TDoc> tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison (->> =)
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let ByField<'TDoc>(tableName, field) =
Find.ByFields<'TDoc>(tableName, Any, Seq.singleton field)
/// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found
[<CompiledName "FSharpFirstByField">]
[<System.Obsolete "Use firstByFields instead ~ will be removed in v4.1">]
let firstByField<'TDoc> tableName field =
Find.firstByFields<'TDoc> tableName Any [ field ]
/// 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">]
let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) =
Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field)
[<RequireQualifiedAccess>]
module Patch =
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field (patch: 'TPatch) =
Patch.byFields tableName Any [ field ] patch
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field fieldNames =
RemoveFields.byFields tableName Any [ field ] fieldNames
[<RequireQualifiedAccess>]
module Delete =
/// Delete documents by matching a JSON field comparison query (->> =)
[<CompiledName "ByField">]
[<System.Obsolete "Use ByFields instead ~ will be removed in v4.1">]
let byField tableName field =
Delete.byFields tableName Any [ field ]
open Microsoft.Data.Sqlite
/// F# Extensions for the NpgsqlConnection type
[<AutoOpen>]
module Extensions =
type SqliteConnection with
/// Count matching documents using a JSON field comparison query (->> =)
[<System.Obsolete "Use countByFields instead ~ will be removed in v4.1">]
member conn.countByField tableName field =
conn.countByFields tableName Any [ field ]
/// Determine if documents exist using a JSON field comparison query (->> =)
[<System.Obsolete "Use existsByFields instead ~ will be removed in v4.1">]
member conn.existsByField tableName field =
conn.existsByFields tableName Any [ field ]
/// Retrieve documents matching a JSON field comparison query (->> =)
[<System.Obsolete "Use findByFields instead ~ will be removed in v4.1">]
member conn.findByField<'TDoc> tableName field =
conn.findByFields<'TDoc> tableName Any [ field ]
/// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found
[<System.Obsolete "Use findFirstByFields instead ~ will be removed in v4.1">]
member conn.findFirstByField<'TDoc> tableName field =
conn.findFirstByFields<'TDoc> tableName Any [ field ]
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<System.Obsolete "Use patchByFields instead ~ will be removed in v4.1">]
member conn.patchByField tableName field (patch: 'TPatch) =
conn.patchByFields tableName Any [ field ] patch
/// Remove fields from documents via a comparison on a JSON field in the document
[<System.Obsolete "Use removeFieldsByFields instead ~ will be removed in v4.1">]
member conn.removeFieldsByField tableName field fieldNames =
conn.removeFieldsByFields tableName Any [ field ] fieldNames
/// Delete documents by matching a JSON field comparison query (->> =)
[<System.Obsolete "Use deleteByFields instead ~ will be removed in v4.1">]
member conn.deleteByField tableName field =
conn.deleteByFields tableName Any [ field ]
open System.Runtime.CompilerServices
type SqliteConnectionCSharpCompatExtensions =
/// Count matching documents using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use CountByFields instead ~ will be removed in v4.1">]
static member inline CountByField(conn, tableName, field) =
WithConn.Count.byFields tableName Any [ field ] conn
/// Determine if documents exist using a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use ExistsByFields instead ~ will be removed in v4.1">]
static member inline ExistsByField(conn, tableName, field) =
WithConn.Exists.byFields tableName Any [ field ] conn
/// Retrieve documents matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use FindByFields instead ~ will be removed in v4.1">]
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithConn.Find.ByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found
[<Extension>]
[<System.Obsolete "Use FindFirstByFields instead ~ will be removed in v4.1">]
static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) =
WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn)
/// Patch documents using a JSON field comparison query in the WHERE clause (->> =)
[<Extension>]
[<System.Obsolete "Use PatchByFields instead ~ will be removed in v4.1">]
static member inline PatchByField(conn, tableName, field, patch: 'TPatch) =
WithConn.Patch.byFields tableName Any [ field ] patch conn
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
[<System.Obsolete "Use RemoveFieldsByFields instead ~ will be removed in v4.1">]
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn
/// Delete documents by matching a JSON field comparison query (->> =)
[<Extension>]
[<System.Obsolete "Use DeleteByFields instead ~ will be removed in v4.1">]
static member inline DeleteByField(conn, tableName, field) =
WithConn.Delete.byFields tableName Any [ field ] conn

View File

@ -1,829 +1,233 @@
namespace BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
open WithConn
/// <summary>F# extensions for the SqliteConnection type</summary>
/// F# extensions for the SqliteConnection type
[<AutoOpen>]
module Extensions =
type SqliteConnection with
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
/// Execute a query that returns a list of results
member conn.customList<'TDoc> query parameters mapFunc =
Custom.list<'TDoc> query parameters mapFunc conn
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
member conn.customJsonArray query parameters mapFunc =
Custom.jsonArray query parameters mapFunc conn
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
member conn.writeCustomJsonArray query parameters writer mapFunc =
Custom.writeJsonArray query parameters writer mapFunc conn
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns><c>Some</c> with the first matching result, or <c>None</c> if not found</returns>
/// Execute a query that returns one or no results
member conn.customSingle<'TDoc> query parameters mapFunc =
Custom.single<'TDoc> query parameters mapFunc conn
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
member conn.customJsonSingle query parameters mapFunc =
Custom.jsonSingle query parameters mapFunc conn
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// Execute a query that does not return a value
member conn.customNonQuery query parameters =
Custom.nonQuery query parameters conn
WithConn.Custom.nonQuery query parameters conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
/// Execute a query that returns a scalar value
member conn.customScalar<'T when 'T: struct> query parameters mapFunc =
Custom.scalar<'T> query parameters mapFunc conn
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// Create a document table
member conn.ensureTable name =
Definition.ensureTable name conn
WithConn.Definition.ensureTable name conn
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// Create an index on a document table
member conn.ensureFieldIndex tableName indexName fields =
Definition.ensureFieldIndex tableName indexName fields conn
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) =
insert<'TDoc> tableName document conn
WithConn.insert<'TDoc> tableName document conn
/// <summary>
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
/// </summary>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
member conn.save<'TDoc> tableName (document: 'TDoc) =
save tableName document conn
WithConn.save tableName document conn
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
/// Count all documents in a table
member conn.countAll tableName =
Count.all tableName conn
WithConn.Count.all tableName conn
/// <summary>Count matching documents using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
member conn.countByFields tableName howMatched fields =
Count.byFields tableName howMatched fields conn
/// Count matching documents using a comparison on a JSON field
member conn.countByField tableName field =
WithConn.Count.byField tableName field conn
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
/// Determine if a document exists for the given ID
member conn.existsById tableName (docId: 'TKey) =
Exists.byId tableName docId conn
WithConn.Exists.byId tableName docId conn
/// <summary>Determine if a document exists using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
member conn.existsByFields tableName howMatched fields =
Exists.byFields tableName howMatched fields conn
/// Determine if a document exists using a comparison on a JSON field
member conn.existsByField tableName field =
WithConn.Exists.byField tableName field conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
/// Retrieve all documents in the given table
member conn.findAll<'TDoc> tableName =
Find.all<'TDoc> tableName conn
WithConn.Find.all<'TDoc> tableName conn
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
member conn.findAllOrdered<'TDoc> tableName orderFields =
Find.allOrdered<'TDoc> tableName orderFields conn
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns><c>Some</c> with the document if found, <c>None</c> otherwise</returns>
/// Retrieve a document by its ID
member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) =
Find.byId<'TKey, 'TDoc> tableName docId conn
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
member conn.findByFields<'TDoc> tableName howMatched fields =
Find.byFields<'TDoc> tableName howMatched fields conn
/// Retrieve documents via a comparison on a JSON field
member conn.findByField<'TDoc> tableName field =
WithConn.Find.byField<'TDoc> tableName field conn
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
member conn.findFirstByField<'TDoc> tableName field =
WithConn.Find.firstByField<'TDoc> tableName field conn
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
member conn.findFirstByFields<'TDoc> tableName howMatched fields =
Find.firstByFields<'TDoc> tableName howMatched fields conn
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the
/// given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found
/// </returns>
member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// <summary>Retrieve all JSON documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All JSON documents from the given table</returns>
member conn.jsonAll tableName =
Json.all tableName conn
/// <summary>Retrieve all JSON documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents from the given table, ordered by the given fields</returns>
member conn.jsonAllOrdered tableName orderFields =
Json.allOrdered tableName orderFields conn
/// <summary>Retrieve a JSON document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The JSON document if found, an empty JSON document otherwise</returns>
member conn.jsonById<'TKey> tableName (docId: 'TKey) =
Json.byId tableName docId conn
/// <summary>Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All JSON documents matching the given fields</returns>
member conn.jsonByFields tableName howMatched fields =
Json.byFields tableName howMatched fields conn
/// <summary>
/// Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents matching the given fields, ordered by the other given fields</returns>
member conn.jsonByFieldsOrdered tableName howMatched queryFields orderFields =
Json.byFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first JSON document if found, an empty JSON document otherwise</returns>
member conn.jsonFirstByFields tableName howMatched fields =
Json.firstByFields tableName howMatched fields conn
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the
/// given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first JSON document (in order) if found, an empty JSON document otherwise</returns>
member conn.jsonFirstByFieldsOrdered tableName howMatched queryFields orderFields =
Json.firstByFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>Write all JSON documents in the given table to the given <c>PipeWriter</c></summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
member conn.writeJsonAll tableName writer =
Json.writeAll tableName writer conn
/// <summary>
/// Write all JSON all documents in the given table to the given <c>PipeWriter</c>, ordered by the given fields
/// in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
member conn.writeJsonAllOrdered tableName writer orderFields =
Json.writeAllOrdered tableName writer orderFields conn
/// <summary>Write a JSON document to the given <c>PipeWriter</c> by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="docId">The ID of the document to retrieve</param>
member conn.writeJsonById<'TKey> tableName writer (docId: 'TKey) =
Json.writeById tableName writer docId conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>,
/// etc.)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
member conn.writeJsonByFields tableName writer howMatched fields =
Json.writeByFields tableName writer howMatched fields conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>,
/// etc.) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
member conn.writeJsonByFieldsOrdered tableName writer howMatched queryFields orderFields =
Json.writeByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
member conn.writeJsonFirstByFields tableName writer howMatched fields =
Json.writeFirstByFields tableName writer howMatched fields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
member conn.writeJsonFirstByFieldsOrdered tableName writer howMatched queryFields orderFields =
Json.writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID
member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) =
Update.byId tableName docId document conn
WithConn.Update.byId tableName docId document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the
/// document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
Update.byFunc tableName idFunc document conn
WithConn.Update.byFunc tableName idFunc document conn
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch a document by its ID
member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) =
Patch.byId tableName docId patch conn
WithConn.Patch.byId tableName docId patch conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <c>WHERE</c> clause (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
member conn.patchByFields tableName howMatched fields (patch: 'TPatch) =
Patch.byFields tableName howMatched fields patch conn
/// Patch documents using a comparison on a JSON field
member conn.patchByField tableName field (patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// Remove fields from a document by the document's ID
member conn.removeFieldsById tableName (docId: 'TKey) fieldNames =
RemoveFields.byId tableName docId fieldNames conn
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
member conn.removeFieldsByFields tableName howMatched fields fieldNames =
RemoveFields.byFields tableName howMatched fields fieldNames conn
/// Remove a field from a document via a comparison on a JSON field in the document
member conn.removeFieldsByField tableName field fieldNames =
WithConn.RemoveFields.byField tableName field fieldNames conn
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// Delete a document by its ID
member conn.deleteById tableName (docId: 'TKey) =
Delete.byId tableName docId conn
WithConn.Delete.byId tableName docId conn
/// <summary>Delete documents by matching a JSON field comparison query (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
member conn.deleteByFields tableName howMatched fields =
Delete.byFields tableName howMatched fields conn
/// Delete documents by matching a comparison on a JSON field
member conn.deleteByField tableName field =
WithConn.Delete.byField tableName field conn
open System.Runtime.CompilerServices
/// <summary>C# extensions on the SqliteConnection type</summary>
/// C# extensions on the SqliteConnection type
type SqliteConnectionCSharpExtensions =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
/// Execute a query that returns a list of results
[<Extension>]
static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
Custom.List<'TDoc>(query, parameters, mapFunc, conn)
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
/// Execute a query that returns one or no results
[<Extension>]
static member inline CustomJsonArray(conn, query, parameters, mapFunc) =
Custom.JsonArray(query, parameters, mapFunc, conn)
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
[<Extension>]
static member inline WriteCustomJsonArray(conn, query, parameters, writer, mapFunc) =
Custom.WriteJsonArray(query, parameters, writer, mapFunc, conn)
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>The first matching result, or <c>null</c> if not found</returns>
[<Extension>]
static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>(
static member inline CustomSingle<'TDoc when 'TDoc: null>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
[<Extension>]
static member inline CustomJsonSingle(conn, query, parameters, mapFunc) =
Custom.JsonSingle(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns no results</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// Execute a query that does not return a value
[<Extension>]
static member inline CustomNonQuery(conn, query, parameters) =
Custom.nonQuery query parameters conn
WithConn.Custom.nonQuery query parameters conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
/// Execute a query that returns a scalar value
[<Extension>]
static member inline CustomScalar<'T when 'T: struct>(
conn, query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
Custom.Scalar<'T>(query, parameters, mapFunc, conn)
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// <summary>Create a document table</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// Create a document table
[<Extension>]
static member inline EnsureTable(conn, name) =
Definition.ensureTable name conn
WithConn.Definition.ensureTable name conn
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// Create an index on one or more fields in a document table
[<Extension>]
static member inline EnsureFieldIndex(conn, tableName, indexName, fields) =
Definition.ensureFieldIndex tableName indexName fields conn
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// <summary>Insert a new document</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// Insert a new document
[<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
insert<'TDoc> tableName document conn
WithConn.insert<'TDoc> tableName document conn
/// <summary>Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
save<'TDoc> tableName document conn
WithConn.save<'TDoc> tableName document conn
/// <summary>Count all documents in a table</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
/// Count all documents in a table
[<Extension>]
static member inline CountAll(conn, tableName) =
Count.all tableName conn
WithConn.Count.all tableName conn
/// <summary>Count matching documents using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
/// Count matching documents using a comparison on a JSON field
[<Extension>]
static member inline CountByFields(conn, tableName, howMatched, fields) =
Count.byFields tableName howMatched fields conn
static member inline CountByField(conn, tableName, field) =
WithConn.Count.byField tableName field conn
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
/// Determine if a document exists for the given ID
[<Extension>]
static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) =
Exists.byId tableName docId conn
WithConn.Exists.byId tableName docId conn
/// <summary>Determine if a document exists using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
/// Determine if a document exists using a comparison on a JSON field
[<Extension>]
static member inline ExistsByFields(conn, tableName, howMatched, fields) =
Exists.byFields tableName howMatched fields conn
static member inline ExistsByField(conn, tableName, field) =
WithConn.Exists.byField tableName field conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
/// Retrieve all documents in the given table
[<Extension>]
static member inline FindAll<'TDoc>(conn, tableName) =
Find.All<'TDoc>(tableName, conn)
WithConn.Find.All<'TDoc>(tableName, conn)
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
/// Retrieve a document by its ID
[<Extension>]
static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) =
Find.AllOrdered<'TDoc>(tableName, orderFields, conn)
static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) =
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// <summary>Retrieve a document by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The document if found, <c>null</c> otherwise</returns>
/// Retrieve documents via a comparison on a JSON field
[<Extension>]
static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) =
Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
static member inline FindByField<'TDoc>(conn, tableName, field) =
WithConn.Find.ByField<'TDoc>(tableName, field, conn)
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<Extension>]
static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) =
Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) =
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn)
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in
/// the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<Extension>]
static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) =
Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
[<Extension>]
static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, fields) =
Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
[<Extension>]
static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
conn, tableName, howMatched, queryFields, orderFields) =
Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Retrieve all JSON documents in the given table</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All JSON documents from the given table</returns>
[<Extension>]
static member inline JsonAll(conn, tableName) =
Json.all tableName conn
/// <summary>Retrieve all JSON documents in the given table ordered by the given fields in the document</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents from the given table, ordered by the given fields</returns>
[<Extension>]
static member inline JsonAllOrdered(conn, tableName, orderFields) =
Json.allOrdered tableName orderFields conn
/// <summary>Retrieve a JSON document by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The JSON document if found, an empty JSON document otherwise</returns>
[<Extension>]
static member inline JsonById<'TKey>(conn, tableName, docId: 'TKey) =
Json.byId tableName docId conn
/// <summary>Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All JSON documents matching the given fields</returns>
[<Extension>]
static member inline JsonByFields(conn, tableName, howMatched, fields) =
Json.byFields tableName howMatched fields conn
/// <summary>
/// Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields
/// in the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents matching the given fields, ordered by the other given fields</returns>
[<Extension>]
static member inline JsonByFieldsOrdered(conn, tableName, howMatched, queryFields, orderFields) =
Json.byFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first JSON document if found, an empty JSON document otherwise</returns>
[<Extension>]
static member inline JsonFirstByFields(conn, tableName, howMatched, fields) =
Json.firstByFields tableName howMatched fields conn
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first JSON document (in order) if found, an empty JSON document otherwise</returns>
[<Extension>]
static member inline JsonFirstByFieldsOrdered(conn, tableName, howMatched, queryFields, orderFields) =
Json.firstByFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>Write all JSON documents in the given table to the given <c>PipeWriter</c></summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
[<Extension>]
static member inline WriteJsonAll(conn, tableName, writer) =
Json.writeAll tableName writer conn
/// <summary>
/// Write all JSON all documents in the given table to the given <c>PipeWriter</c>, ordered by the given fields in
/// the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<Extension>]
static member inline WriteJsonAllOrdered(conn, tableName, writer, orderFields) =
Json.writeAllOrdered tableName writer orderFields conn
/// <summary>Write a JSON document to the given <c>PipeWriter</c> by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="docId">The ID of the document to retrieve</param>
[<Extension>]
static member inline WriteJsonById<'TKey>(conn, tableName, writer, docId: 'TKey) =
Json.writeById tableName writer docId conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<Extension>]
static member inline WriteJsonByFields(conn, tableName, writer, howMatched, fields) =
Json.writeByFields tableName writer howMatched fields conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// ordered by the given fields in the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<Extension>]
static member inline WriteJsonByFieldsOrdered(conn, tableName, writer, howMatched, queryFields, orderFields) =
Json.writeByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<Extension>]
static member inline WriteJsonFirstByFields(conn, tableName, writer, howMatched, fields) =
Json.writeFirstByFields tableName writer howMatched fields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<Extension>]
static member inline WriteJsonFirstByFieldsOrdered(conn, tableName, writer, howMatched, queryFields, orderFields) =
Json.writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID
[<Extension>]
static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) =
Update.byId tableName docId document conn
WithConn.Update.byId tableName docId document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<Extension>]
static member inline UpdateByFunc<'TKey, 'TDoc>(
conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
Update.ByFunc(tableName, idFunc, document, conn)
static member inline UpdateByFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) =
WithConn.Update.ByFunc(tableName, idFunc, doc, conn)
/// <summary>Patch a document by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch a document by its ID
[<Extension>]
static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) =
Patch.byId tableName docId patch conn
WithConn.Patch.byId tableName docId patch conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <c>WHERE</c> clause (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// Patch documents using a comparison on a JSON field
[<Extension>]
static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) =
Patch.byFields tableName howMatched fields patch conn
static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) =
WithConn.Patch.byField tableName field patch conn
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// Remove fields from a document by the document's ID
[<Extension>]
static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) =
RemoveFields.byId tableName docId fieldNames conn
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn)
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// Remove fields from documents via a comparison on a JSON field in the document
[<Extension>]
static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) =
RemoveFields.byFields tableName howMatched fields fieldNames conn
static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) =
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn)
/// <summary>Delete a document by its ID</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// Delete a document by its ID
[<Extension>]
static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) =
Delete.byId tableName docId conn
WithConn.Delete.byId tableName docId conn
/// <summary>Delete documents by matching a JSON field comparison query (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="conn">The <c>SqliteConnection</c> on which to run the query</param>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// Delete documents by matching a comparison on a JSON field
[<Extension>]
static member inline DeleteByFields(conn, tableName, howMatched, fields) =
Delete.byFields tableName howMatched fields conn
static member inline DeleteByField(conn, tableName, field) =
WithConn.Delete.byField tableName field conn

View File

@ -1,636 +0,0 @@
namespace BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
/// <summary>Commands to execute custom SQL queries</summary>
[<RequireQualifiedAccess>]
module Custom =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>A list of results for the given query</returns>
let List<'TDoc>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn ()
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
[<CompiledName "FSharpJsonArray">]
let jsonArray query parameters mapFunc =
use conn = Configuration.dbConn ()
WithConn.Custom.jsonArray query parameters mapFunc conn
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>A JSON array of results for the given query</returns>
let JsonArray(query, parameters, mapFunc) =
use conn = Configuration.dbConn ()
WithConn.Custom.JsonArray(query, parameters, mapFunc, conn)
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
[<CompiledName "FSharpWriteJsonArray">]
let writeJsonArray query parameters writer mapFunc =
use conn = Configuration.dbConn ()
WithConn.Custom.writeJsonArray query parameters writer mapFunc conn
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
let WriteJsonArray(query, parameters, writer, mapFunc) =
use conn = Configuration.dbConn ()
WithConn.Custom.WriteJsonArray(query, parameters, writer, mapFunc, conn)
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns><c>Some</c> with the first matching result, or <c>None</c> if not found</returns>
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <returns>The first matching result, or <c>null</c> if not found</returns>
let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
[<CompiledName "FSharpJsonSingle">]
let jsonSingle query parameters mapFunc =
use conn = Configuration.dbConn ()
WithConn.Custom.jsonSingle query parameters mapFunc conn
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
let JsonSingle(query, parameters, mapFunc) =
use conn = Configuration.dbConn ()
WithConn.Custom.JsonSingle(query, parameters, mapFunc, conn)
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
[<CompiledName "NonQuery">]
let nonQuery query parameters =
use conn = Configuration.dbConn ()
WithConn.Custom.nonQuery query parameters conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: SqliteDataReader -> 'T) =
use conn = Configuration.dbConn ()
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <returns>The scalar value for the query</returns>
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// <summary>Functions to create tables and indexes</summary>
[<RequireQualifiedAccess>]
module Definition =
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
[<CompiledName "EnsureTable">]
let ensureTable name =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureTable name conn
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// <summary>Document insert/save functions</summary>
[<AutoOpen>]
module Document =
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Document.insert tableName document conn
/// <summary>Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")</summary>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Document.save tableName document conn
/// <summary>Commands to count documents</summary>
[<RequireQualifiedAccess>]
module Count =
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "All">]
let all tableName =
use conn = Configuration.dbConn ()
WithConn.Count.all tableName conn
/// <summary>Count matching documents using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The count of matching documents in the table</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Count.byFields tableName howMatched fields conn
/// <summary>Commands to determine if documents exist</summary>
[<RequireQualifiedAccess>]
module Exists =
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <returns>True if a document exists, false if not</returns>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Exists.byId tableName docId conn
/// <summary>Determine if a document exists using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Exists.byFields tableName howMatched fields conn
/// <summary>Commands to retrieve documents</summary>
[<RequireQualifiedAccess>]
module Find =
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.all<'TDoc> tableName conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All documents from the given table</returns>
let All<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.All<'TDoc>(tableName, conn)
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.allOrdered<'TDoc> tableName orderFields conn
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
let AllOrdered<'TDoc> tableName orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn)
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns><c>Some</c> with the document if found, <c>None</c> otherwise</returns>
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
use conn = Configuration.dbConn ()
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The document if found, <c>null</c> otherwise</returns>
let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId) =
use conn = Configuration.dbConn ()
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Find.byFields<'TDoc> tableName howMatched fields conn
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All documents matching the given fields</returns>
let ByFields<'TDoc>(tableName, howMatched, fields) =
use conn = Configuration.dbConn ()
WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn)
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "FSharpByFieldsOrdered">]
let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) =
use conn = Configuration.dbConn ()
WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn)
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>
/// <c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found
/// </returns>
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn)
/// <summary>Commands to retrieve documents as raw JSON</summary>
[<RequireQualifiedAccess>]
module Json =
/// <summary>Retrieve all JSON documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <returns>All JSON documents from the given table</returns>
[<CompiledName "All">]
let all tableName =
use conn = Configuration.dbConn ()
WithConn.Json.all tableName conn
/// <summary>Retrieve all JSON documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents from the given table, ordered by the given fields</returns>
[<CompiledName "AllOrdered">]
let allOrdered tableName orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.allOrdered tableName orderFields conn
/// <summary>Retrieve a JSON document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <returns>The JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "ById">]
let byId<'TKey> tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Json.byId tableName docId conn
/// <summary>Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>All JSON documents matching the given fields</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Json.byFields tableName howMatched fields conn
/// <summary>
/// Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields
/// in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>All JSON documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "ByFieldsOrdered">]
let byFieldsOrdered tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.byFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <returns>The first JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFields">]
let firstByFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Json.firstByFields tableName howMatched fields conn
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <returns>The first JSON document (in order) if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFieldsOrdered">]
let firstByFieldsOrdered tableName howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.firstByFieldsOrdered tableName howMatched queryFields orderFields conn
/// <summary>Write all JSON documents in the given table to the given <c>PipeWriter</c></summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
[<CompiledName "WriteAll">]
let writeAll tableName writer =
use conn = Configuration.dbConn ()
WithConn.Json.writeAll tableName writer conn
/// <summary>
/// Write all JSON all documents in the given table to the given <c>PipeWriter</c>, ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteAllOrdered">]
let writeAllOrdered tableName writer orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.writeAllOrdered tableName writer orderFields conn
/// <summary>Write a JSON document to the given <c>PipeWriter</c> by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="docId">The ID of the document to retrieve</param>
[<CompiledName "WriteById">]
let writeById<'TKey> tableName writer (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Json.writeById tableName writer docId conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "WriteByFields">]
let writeByFields tableName writer howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Json.writeByFields tableName writer howMatched fields conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteByFieldsOrdered">]
let writeByFieldsOrdered tableName writer howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.writeByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "WriteFirstByFields">]
let writeFirstByFields tableName writer howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Json.writeFirstByFields tableName writer howMatched fields conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
[<CompiledName "WriteFirstByFieldsOrdered">]
let writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields =
use conn = Configuration.dbConn ()
WithConn.Json.writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields conn
/// <summary>Commands to update documents</summary>
[<RequireQualifiedAccess>]
module Update =
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byId tableName docId document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byFunc tableName idFunc document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.ByFunc(tableName, idFunc, document, conn)
/// <summary>Commands to patch (partially update) documents</summary>
[<RequireQualifiedAccess>]
module Patch =
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byId tableName docId patch conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <c>WHERE</c> clause (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byFields tableName howMatched fields patch conn
/// <summary>Commands to remove fields from documents</summary>
[<RequireQualifiedAccess>]
module RemoveFields =
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn
/// <summary>Commands to delete documents</summary>
[<RequireQualifiedAccess>]
module Delete =
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Delete.byId tableName docId conn
/// <summary>Delete documents by matching a JSON field comparison query (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields =
use conn = Configuration.dbConn ()
WithConn.Delete.byFields tableName howMatched fields conn

View File

@ -1,26 +1,22 @@
namespace BitBadger.Documents.Sqlite
open BitBadger.Documents
open Microsoft.Data.Sqlite
/// <summary>Configuration for document handling</summary>
/// Configuration for document handling
module Configuration =
/// The connection string to use for query execution
let mutable internal connectionString: string option = None
/// <summary>Register a connection string to use for query execution</summary>
/// <param name="connStr">The connection string to use for connections from this library</param>
/// <remarks>This also enables foreign keys</remarks>
/// Register a connection string to use for query execution (enables foreign keys)
[<CompiledName "UseConnectionString">]
let useConnectionString connStr =
let builder = SqliteConnectionStringBuilder connStr
builder.ForeignKeys <- Option.toNullable (Some true)
connectionString <- Some (string builder)
/// <summary>Retrieve a new connection using currently configured connection string</summary>
/// <returns>A new database connection</returns>
/// <exception cref="T:System.InvalidOperationException">If no data source has been configured</exception>
/// <exception cref="SqliteException">If the connection cannot be opened</exception>
/// Retrieve the currently configured data source
[<CompiledName "DbConn">]
let dbConn () =
match connectionString with
@ -31,184 +27,203 @@ module Configuration =
| None -> invalidOp "Please provide a connection string before attempting data access"
open BitBadger.Documents
/// <summary>Query definitions</summary>
/// Query definitions
[<RequireQualifiedAccess>]
module Query =
/// <summary>Create a <c>WHERE</c> clause fragment to implement a comparison on fields in a JSON document</summary>
/// <param name="howMatched">How the fields should be matched</param>
/// <param name="fields">The fields for the comparisons</param>
/// <returns>A <c>WHERE</c> clause implementing the comparisons for the given fields</returns>
[<CompiledName "WhereByFields">]
let whereByFields (howMatched: FieldMatch) fields =
let name = ParameterName()
fields
|> Seq.map (fun it ->
match it.Comparison with
| Exists | NotExists -> $"{it.Path SQLite AsSql} {it.Comparison.OpSql}"
| Between _ ->
let p = name.Derive it.ParameterName
$"{it.Path SQLite AsSql} {it.Comparison.OpSql} {p}min AND {p}max"
| In values ->
let p = name.Derive it.ParameterName
let paramNames = values |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", "
$"{it.Path SQLite AsSql} {it.Comparison.OpSql} ({paramNames})"
| InArray (table, values) ->
let p = name.Derive it.ParameterName
let paramNames = values |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", "
$"EXISTS (SELECT 1 FROM json_each({table}.data, '$.{it.Name}') WHERE value IN ({paramNames}))"
| _ -> $"{it.Path SQLite AsSql} {it.Comparison.OpSql} {name.Derive it.ParameterName}")
|> String.concat $" {howMatched} "
/// 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}"
/// <summary>Create a <c>WHERE</c> clause fragment to implement an ID-based query</summary>
/// <param name="docId">The ID of the document</param>
/// <returns>A <c>WHERE</c> clause fragment identifying a document by its ID</returns>
/// Create a WHERE clause fragment to implement an ID-based query
[<CompiledName "WhereById">]
let whereById (docId: 'TKey) =
whereByFields Any [ { Field.Equal (Configuration.idField ()) docId with ParameterName = Some "@id" } ]
let whereById paramName =
whereByField (Field.EQ (Configuration.idField ()) 0) paramName
/// <summary>Create an <c>UPDATE</c> statement to patch documents</summary>
/// <param name="tableName">The table to be updated</param>
/// <returns>A query to patch documents</returns>
[<CompiledName "Patch">]
let patch tableName =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data))"
/// <summary>Create an <c>UPDATE</c> statement to remove fields from documents</summary>
/// <param name="tableName">The table to be updated</param>
/// <param name="parameters">The parameters with the field names to be removed</param>
/// <returns>A query to remove fields from documents</returns>
[<CompiledName "RemoveFields">]
let removeFields tableName (parameters: SqliteParameter seq) =
let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames})"
/// <summary>Create a query by a document's ID</summary>
/// <param name="statement">The SQL statement to be run against a document by its ID</param>
/// <param name="docId">The ID of the document targeted</param>
/// <returns>A query addressing a document by its ID</returns>
[<CompiledName "ById">]
let byId<'TKey> statement (docId: 'TKey) =
Query.statementWhere
statement
(whereByFields Any [ { Field.Equal (Configuration.idField ()) docId with ParameterName = Some "@id" } ])
/// <summary>Create a query on JSON fields</summary>
/// <param name="statement">The SQL statement to be run against matching fields</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to be matched</param>
/// <returns>A query addressing documents by field matching conditions</returns>
[<CompiledName "ByFields">]
let byFields statement howMatched fields =
Query.statementWhere statement (whereByFields howMatched fields)
/// <summary>Data definition</summary>
/// Data definition
module Definition =
/// <summary>SQL statement to create a document table</summary>
/// <param name="name">The name of the table (may include schema)</param>
/// <returns>A query to create the table if it does not exist</returns>
/// SQL statement to create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
/// Query to update a document
[<CompiledName "Update">]
let update tableName =
$"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}"""
/// <summary>Parameter handling helpers</summary>
/// Queries for counting documents
module Count =
/// Query to count all documents in a table
[<CompiledName "All">]
let all tableName =
$"SELECT COUNT(*) AS it FROM %s{tableName}"
/// Query to count matching documents using a text comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Queries for determining document existence
module Exists =
/// Query to determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it"""
/// Query to determine if documents exist using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it"""
/// Queries for retrieving documents
module Find =
/// Query to retrieve a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""{Query.selectFromTable tableName} WHERE {whereById "@id"}"""
/// Query to retrieve documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}"""
/// Document patching (partial update) queries
module Patch =
/// Create an UPDATE statement to patch documents
let internal update tableName whereClause =
$"UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE %s{whereClause}"
/// Query to patch (partially update) a document by its ID
[<CompiledName "ById">]
let byId tableName =
whereById "@id" |> update tableName
/// Query to patch (partially update) a document via a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
whereByField field "@field" |> update tableName
/// Queries to remove fields from documents
module RemoveFields =
/// Create an UPDATE statement to remove parameters
let internal update tableName (parameters: SqliteParameter list) whereClause =
let paramNames = parameters |> List.map _.ParameterName |> String.concat ", "
$"UPDATE %s{tableName} SET data = json_remove(data, {paramNames}) WHERE {whereClause}"
/// Query to remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName parameters =
whereById "@id" |> update tableName parameters
/// Query to remove fields from a document by the document's ID
let ById(tableName, parameters) =
byId tableName (List.ofSeq parameters)
/// Query to remove fields from documents via a comparison on a JSON field within the document
[<CompiledName "FSharpByField">]
let byField tableName field parameters =
whereByField field "@field" |> update tableName parameters
/// Query to remove fields from documents via a comparison on a JSON field within the document
let ByField(tableName, field, parameters) =
byField tableName field (List.ofSeq parameters)
/// Queries to delete documents
module Delete =
/// Query to delete a document by its ID
[<CompiledName "ById">]
let byId tableName =
$"""DELETE FROM %s{tableName} WHERE {whereById "@id"}"""
/// Query to delete documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
$"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}"""
/// Parameter handling helpers
[<AutoOpen>]
module Parameters =
/// <summary>Create an ID parameter (name "@id")</summary>
/// <param name="key">The key value for the ID parameter</param>
/// <returns>The name and parameter value for the ID</returns>
/// Create an ID parameter (name "@id", key will be treated as a string)
[<CompiledName "Id">]
let idParam (key: 'TKey) =
SqliteParameter("@id", string key)
/// <summary>Create a parameter with a JSON value</summary>
/// <param name="name">The name of the parameter to create</param>
/// <param name="it">The criteria to provide as JSON</param>
/// <returns>The name and parameter value for the JSON field</returns>
/// Create a parameter with a JSON value
[<CompiledName "Json">]
let jsonParam name (it: 'TJson) =
SqliteParameter(name, Configuration.serializer().Serialize it)
/// <summary>Create JSON field parameters</summary>
/// <param name="fields">The <c>Field</c>s to convert to parameters</param>
/// <param name="parameters">The current parameters for the query</param>
/// <returns>A unified sequence of parameter names and values</returns>
[<CompiledName "AddFields">]
let addFieldParams fields parameters =
let name = ParameterName()
fields
|> Seq.map (fun it ->
seq {
match it.Comparison with
| Exists | NotExists -> ()
| Between (min, max) ->
let p = name.Derive it.ParameterName
yield! [ SqliteParameter($"{p}min", min); SqliteParameter($"{p}max", max) ]
| In values | InArray (_, values) ->
let p = name.Derive it.ParameterName
yield! values |> Seq.mapi (fun idx v -> SqliteParameter($"{p}_{idx}", v))
| Equal v | Greater v | GreaterOrEqual v | Less v | LessOrEqual v | NotEqual v ->
yield SqliteParameter(name.Derive it.ParameterName, v) })
|> Seq.collect id
|> Seq.append parameters
|> Seq.toList
|> Seq.ofList
/// Create a JSON field parameter (name "@field")
[<CompiledName "FSharpAddField">]
let addFieldParam name field 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")
[<CompiledName "AddField">]
[<System.Obsolete "Use addFieldParams instead; will be removed in v4">]
let addFieldParam name field parameters =
addFieldParams [ { field with ParameterName = Some name } ] parameters
let AddField(name, field, parameters) =
match field.Op with
| EX | NEX -> parameters
| BT ->
let values = field.Value :?> obj list
// let min = SqliteParameter($"{name}min", SqliteType.Integer)
// min.Value <- values[0]
// let max = SqliteParameter($"{name}max", SqliteType.Integer)
// max.Value <- values[1]
[ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ]
// [min; max]
|> Seq.append parameters
| _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters
/// <summary>Append JSON field name parameters for the given field names to the given parameters</summary>
/// <param name="paramName">The name of the parameter to use for each field</param>
/// <param name="fieldNames">The names of fields to be addressed</param>
/// <returns>The name (<c>@name</c>) and parameter value for the field names</returns>
[<CompiledName "FieldNames">]
let fieldNameParams paramName fieldNames =
fieldNames
|> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.%s{name}"))
|> Seq.toList
|> Seq.ofList
/// Append JSON field name parameters for the given field names to the given parameters
[<CompiledName "FSharpFieldNames">]
let fieldNameParams paramName (fieldNames: string list) =
fieldNames |> List.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}"))
/// <summary>An empty parameter sequence</summary>
/// Append JSON field name parameters for the given field names to the given parameters
let FieldNames(paramName, fieldNames: string seq) =
fieldNames |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}"))
/// An empty parameter sequence
[<CompiledName "None">]
let noParams =
Seq.empty<SqliteParameter>
open System.Text
/// <summary>Helper functions for handling results</summary>
/// Helper functions for handling results
[<AutoOpen>]
module Results =
/// <summary>Create a domain item from a document, specifying the field in which the document is found</summary>
/// <param name="field">The field name containing the JSON document</param>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the document to be constructed</param>
/// <returns>The constructed domain item</returns>
/// Create a domain item from a document, specifying the field in which the document is found
[<CompiledName "FromDocument">]
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)))
/// <summary>Create a domain item from a document</summary>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the document to be constructed</param>
/// <returns>The constructed domain item</returns>
/// Create a domain item from a document
[<CompiledName "FromData">]
let fromData<'TDoc> rdr =
fromDocument<'TDoc> "data" rdr
/// <summary>
/// Create a list of items for the results of the given command, using the specified mapping function
/// </summary>
/// <param name="cmd">The command to execute</param>
/// <param name="mapFunc">The mapping function from data reader to domain class instance</param>
/// <returns>A list of items from the reader</returns>
[<CompiledName "FSharpToCustomList">]
let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask {
use! rdr = cmd.ExecuteReaderAsync()
@ -218,104 +233,489 @@ module Results =
return List.ofSeq it
}
/// <summary>
/// Create a list of items for the results of the given command, using the specified mapping function
/// </summary>
/// <param name="cmd">The command to execute</param>
/// <param name="mapFunc">The mapping function from data reader to domain class instance</param>
/// <returns>A list of items from the reader</returns>
let ToCustomList<'TDoc>(cmd: SqliteCommand, mapFunc: System.Func<SqliteDataReader, 'TDoc>) = backgroundTask {
use! rdr = cmd.ExecuteReaderAsync()
let it = ResizeArray<'TDoc>()
while! rdr.ReadAsync() do
it.Add(mapFunc.Invoke rdr)
return it
}
/// <summary>Extract a count from the first column</summary>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the count to retrieve</param>
/// <returns>The count from the row</returns>
/// Extract a count from the first column
[<CompiledName "ToCount">]
let toCount (rdr: SqliteDataReader) =
rdr.GetInt64 0
let toCount (row: SqliteDataReader) =
row.GetInt64 0
/// <summary>Extract a true/false value from the first column</summary>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the true/false value to retrieve</param>
/// <returns>The true/false value from the row</returns>
/// <remarks>SQLite implements boolean as 1 = true, 0 = false</remarks>
/// Extract a true/false value from a count in the first column
[<CompiledName "ToExists">]
let toExists rdr =
toCount rdr > 0L
/// <summary>Retrieve a JSON document, specifying the field in which the document is found</summary>
/// <param name="field">The field name containing the JSON document</param>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the document to be constructed</param>
/// <returns>The JSON document (an empty JSON document if not found)</returns>
[<CompiledName "JsonFromDocument">]
let jsonFromDocument field (rdr: SqliteDataReader) =
try
let idx = rdr.GetOrdinal field
if rdr.IsDBNull idx then "{}" else rdr.GetString idx
with :? System.IndexOutOfRangeException -> "{}"
/// <summary>Retrieve a JSON document</summary>
/// <param name="rdr">A <c>SqliteDataReader</c> set to the row with the document to be constructed</param>
/// <returns>The JSON document (an empty JSON document if not found)</returns>
[<CompiledName "JsonFromData">]
let jsonFromData rdr =
jsonFromDocument "data" rdr
/// <summary>
/// Create a JSON array for the results of the given command, using the specified mapping function
/// </summary>
/// <param name="cmd">The command to execute</param>
/// <param name="mapFunc">The mapping function to extract JSON from the query's results</param>
/// <returns>A JSON array of items from the reader</returns>
[<CompiledName "FSharpToJsonArray">]
let toJsonArray (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> string) = backgroundTask {
use! rdr = cmd.ExecuteReaderAsync()
let it = StringBuilder "["
while! rdr.ReadAsync() do
if it.Length > 2 then ignore (it.Append ",")
it.Append(mapFunc rdr) |> ignore
return it.Append("]").ToString()
}
/// <summary>
/// Create a JSON array for the results of the given command, using the specified mapping function
/// </summary>
/// <param name="cmd">The command to execute</param>
/// <param name="mapFunc">The mapping function to extract JSON from the query's results</param>
/// <returns>A JSON array of items from the reader</returns>
let ToJsonArray (cmd: SqliteCommand) (mapFunc: System.Func<SqliteDataReader, string>) =
toJsonArray cmd mapFunc.Invoke
/// <summary>Write a JSON array of items for the results of a query to the given <c>StreamWriter</c></summary>
/// <param name="cmd">The command to execute</param>
/// <param name="writer">The StreamWriter to which results should be written</param>
/// <param name="mapFunc">The mapping function to extract JSON from the query's results</param>
[<CompiledName "FSharpWriteJsonArray">]
let writeJsonArray (cmd: SqliteCommand) writer (mapFunc: SqliteDataReader -> string) = backgroundTask {
use! rdr = cmd.ExecuteReaderAsync()
return
seq { while rdr.Read() do yield mapFunc rdr }
|> PipeWriter.writeStrings writer
}
/// <summary>Write a JSON array of items for the results of a query to the given <c>StreamWriter</c></summary>
/// <param name="cmd">The command to execute</param>
/// <param name="writer">The StreamWriter to which results should be written</param>
/// <param name="mapFunc">The mapping function to extract JSON from the query's results</param>
let WriteJsonArray (cmd: SqliteCommand) writer (mapFunc: System.Func<SqliteDataReader, string>) =
writeJsonArray cmd writer mapFunc.Invoke
let toExists row =
toCount(row) > 0L
[<AutoOpen>]
module internal Helpers =
/// <summary>Execute a non-query command</summary>
/// <param name="cmd">The command to be executed</param>
/// Execute a non-query command
let internal write (cmd: SqliteCommand) = backgroundTask {
let! _ = cmd.ExecuteNonQueryAsync()
()
}
/// Versions of queries that accept a SqliteConnection as the last parameter
module WithConn =
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'TDoc)
(conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
toCustomList<'TDoc> cmd mapFunc
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn) = backgroundTask {
let! results = list<'TDoc> query parameters mapFunc.Invoke conn
return ResizeArray<'TDoc> results
}
/// Execute a query that returns one or no results (returns None if not found)
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) conn = backgroundTask {
let! results = list query parameters mapFunc conn
return FSharp.Collections.List.tryHead results
}
/// 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>, conn
) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke conn
return Option.toObj result
}
/// Execute a query that does not return a value
[<CompiledName "NonQuery">]
let nonQuery query (parameters: SqliteParameter seq) (conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
write cmd
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T : struct> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'T)
(conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
use! rdr = cmd.ExecuteReaderAsync()
let! isFound = rdr.ReadAsync()
return if isFound then mapFunc rdr else Unchecked.defaultof<'T>
}
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>, conn) =
scalar<'T> query parameters mapFunc.Invoke conn
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name conn = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn
}
/// Create an index on a document table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn
/// Commands to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName conn =
Custom.scalar (Query.Count.all tableName) [] toCount conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field conn =
Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field conn =
Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists conn
/// Commands to retrieve documents
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName conn =
Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn
/// Retrieve all documents in the given table
let All<'TDoc>(tableName, conn) =
Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn)
/// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn =
Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) =
Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on a JSON field
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName field conn =
Custom.list<'TDoc>
(Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> conn
/// Retrieve documents via a comparison on a JSON field
let ByField<'TDoc>(tableName, field, conn) =
Custom.List<'TDoc>(
Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName field conn =
Custom.single
$"{Query.Find.byField tableName field} LIMIT 1" (addFieldParam "@field" field []) fromData<'TDoc> conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) =
Custom.Single(
$"{Query.Find.byField tableName field} LIMIT 1", addFieldParam "@field" field [], fromData<'TDoc>, conn)
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) conn =
Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn =
byId tableName (idFunc document) document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) =
byFunc tableName idFunc.Invoke document conn
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) conn =
Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] conn
/// Patch documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) =
Custom.nonQuery
(Query.Patch.byField tableName field) (addFieldParam "@field" field [ jsonParam "@data" patch ]) conn
/// Commands to remove fields from documents
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName (docId: 'TKey) fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery (Query.RemoveFields.byId tableName nameParams) (idParam docId :: nameParams) conn
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames, conn) =
byId tableName docId (List.ofSeq fieldNames) conn
/// Remove fields from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery
(Query.RemoveFields.byField tableName field nameParams) (addFieldParam "@field" field nameParams) conn
/// Remove fields from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames, conn) =
byField tableName field (List.ofSeq fieldNames) conn
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field conn =
Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) conn
/// Commands to execute custom SQL queries
[<RequireQualifiedAccess>]
module Custom =
/// Execute a query that returns a list of results
[<CompiledName "FSharpList">]
let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.list<'TDoc> query parameters mapFunc conn
/// Execute a query that returns a list of results
let List<'TDoc>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>) =
use conn = Configuration.dbConn ()
WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that returns one or no results (returns None if not found)
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Custom.single<'TDoc> query parameters mapFunc conn
/// 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>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn)
/// Execute a query that does not return a value
[<CompiledName "NonQuery">]
let nonQuery query parameters =
use conn = Configuration.dbConn ()
WithConn.Custom.nonQuery query parameters conn
/// Execute a query that returns a scalar value
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T: struct> query parameters (mapFunc: SqliteDataReader -> 'T) =
use conn = Configuration.dbConn ()
WithConn.Custom.scalar<'T> query parameters mapFunc conn
/// Execute a query that returns a scalar value
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>) =
use conn = Configuration.dbConn ()
WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn)
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureTable name conn
/// Create an index on a document table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureFieldIndex tableName indexName fields conn
/// Document insert/save functions
[<AutoOpen>]
module Document =
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.insert tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.save tableName document conn
/// Commands to count documents
[<RequireQualifiedAccess>]
module Count =
/// Count all documents in a table
[<CompiledName "All">]
let all tableName =
use conn = Configuration.dbConn ()
WithConn.Count.all tableName conn
/// Count matching documents using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Count.byField tableName field conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Exists =
/// Determine if a document exists for the given ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Exists.byId tableName docId conn
/// Determine if a document exists using a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Exists.byField tableName field conn
/// Commands to determine if documents exist
[<RequireQualifiedAccess>]
module Find =
/// Retrieve all documents in the given table
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.all<'TDoc> tableName conn
/// Retrieve all documents in the given table
let All<'TDoc> tableName =
use conn = Configuration.dbConn ()
WithConn.Find.All<'TDoc>(tableName, conn)
/// Retrieve a document by its ID (returns None if not found)
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName docId =
use conn = Configuration.dbConn ()
WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn
/// Retrieve a document by its ID (returns null if not found)
let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId) =
use conn = Configuration.dbConn ()
WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn)
/// Retrieve documents via a comparison on a JSON field
[<CompiledName "FSharpByField">]
let byField<'TDoc> tableName field =
use conn = Configuration.dbConn ()
WithConn.Find.byField<'TDoc> tableName field conn
/// Retrieve documents via a comparison on a JSON field
let ByField<'TDoc>(tableName, field) =
use conn = Configuration.dbConn ()
WithConn.Find.ByField<'TDoc>(tableName, field, conn)
/// Retrieve documents via a comparison on a JSON field, returning only the first result
[<CompiledName "FSharpFirstByField">]
let firstByField<'TDoc> tableName field =
use conn = Configuration.dbConn ()
WithConn.Find.firstByField<'TDoc> tableName field conn
/// Retrieve documents via a comparison on a JSON field, returning only the first result
let FirstByField<'TDoc when 'TDoc: null>(tableName, field) =
use conn = Configuration.dbConn ()
WithConn.Find.FirstByField<'TDoc>(tableName, field, conn)
/// Commands to update documents
[<RequireQualifiedAccess>]
module Update =
/// Update an entire document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byId tableName docId document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.byFunc tableName idFunc document conn
/// Update an entire document by its ID, using the provided function to obtain the ID from the document
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Update.ByFunc(tableName, idFunc, document, conn)
/// Commands to patch (partially update) documents
[<RequireQualifiedAccess>]
module Patch =
/// Patch a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byId tableName docId patch conn
/// Patch documents using a comparison on a JSON field in the WHERE clause
[<CompiledName "ByField">]
let byField tableName field (patch: 'TPatch) =
use conn = Configuration.dbConn ()
WithConn.Patch.byField tableName field patch conn
/// Commands to remove fields from documents
[<RequireQualifiedAccess>]
module RemoveFields =
/// Remove fields from a document by the document's ID
[<CompiledName "FSharpById">]
let byId tableName (docId: 'TKey) fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byId tableName docId fieldNames conn
/// Remove fields from a document by the document's ID
let ById(tableName, docId: 'TKey, fieldNames) =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn)
/// Remove field from documents via a comparison on a JSON field in the document
[<CompiledName "FSharpByField">]
let byField tableName field fieldNames =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.byField tableName field fieldNames conn
/// Remove field from documents via a comparison on a JSON field in the document
let ByField(tableName, field, fieldNames) =
use conn = Configuration.dbConn ()
WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn)
/// Commands to delete documents
[<RequireQualifiedAccess>]
module Delete =
/// Delete a document by its ID
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) =
use conn = Configuration.dbConn ()
WithConn.Delete.byId tableName docId conn
/// Delete documents by matching a comparison on a JSON field
[<CompiledName "ByField">]
let byField tableName field =
use conn = Configuration.dbConn ()
WithConn.Delete.byField tableName field conn

View File

@ -5,16 +5,11 @@ This package provides a lightweight document library backed by [SQLite](https://
## Features
- 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
- 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 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://relationaldocs.bitbadger.solutions/dotnet/upgrade/v4.html) for full details.
## Getting Started
Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library:
@ -77,30 +72,30 @@ Count customers in Atlanta:
```csharp
// C#; parameters are table name, field, operator, and value
// Count.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task<long>>
var customerCount = await Count.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Atlanta")]);
// Count.ByField type signature is Func<string, Field, Task<long>>
var customerCount = await Count.ByField("customer", Field.EQ("City", "Atlanta"));
```
```fsharp
// F#
// Count.byFields type signature is string -> FieldMatch -> Field seq -> Task<int64>
let! customerCount = Count.byFields "customer" Any [ Field.Equal "City" "Atlanta" ]
// Count.byField type signature is string -> Field -> Task<int64>
let! customerCount = Count.byField "customer" (Field.EQ "City" "Atlanta")
```
Delete customers in Chicago: _(no offense, Second City; just an example...)_
```csharp
// C#; parameters are same as above, except return is void
// Delete.ByFields type signature is Func<string, FieldMatch, IEnumerable<Field>, Task>
await Delete.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Chicago")]);
// Delete.ByField type signature is Func<string, Field, Task>
await Delete.ByField("customer", Field.EQ("City", "Chicago"));
```
```fsharp
// F#
// Delete.byFields type signature is string -> FieldMatch -> Field seq -> Task<unit>
do! Delete.byFields "customer" Any [ Field.Equal "City" "Chicago" ]
// Delete.byField type signature is string -> string -> Op -> obj -> Task<unit>
do! Delete.byField "customer" (Field.EQ "City" "Chicago")
```
## More Information
The [project site](https://relationaldocs.bitbadger.solutions/dotnet/) has full details on how to use this library.
The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library.

View File

@ -1,784 +0,0 @@
/// <summary>Versions of queries that accept a <c>SqliteConnection</c> as the last parameter</summary>
module BitBadger.Documents.Sqlite.WithConn
open BitBadger.Documents
open Microsoft.Data.Sqlite
/// <summary>Commands to execute custom SQL queries</summary>
[<RequireQualifiedAccess>]
module Custom =
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>A list of results for the given query</returns>
[<CompiledName "FSharpList">]
let list<'TDoc> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'TDoc)
(conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
toCustomList<'TDoc> cmd mapFunc
/// <summary>Execute a query that returns a list of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>A list of results for the given query</returns>
let List<'TDoc>(
query, parameters: SqliteParameter seq, mapFunc: System.Func<SqliteDataReader, 'TDoc>,
conn: SqliteConnection
) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
ToCustomList<'TDoc>(cmd, mapFunc)
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>A JSON array of results for the given query</returns>
[<CompiledName "FSharpJsonArray">]
let jsonArray
query
(parameters: SqliteParameter seq)
(mapFunc: SqliteDataReader -> string)
(conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
toJsonArray cmd mapFunc
/// <summary>Execute a query that returns a JSON array of results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>A JSON array of results for the given query</returns>
let JsonArray(query, parameters, mapFunc: System.Func<SqliteDataReader, string>, conn) =
jsonArray query parameters mapFunc.Invoke conn
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "FSharpWriteJsonArray">]
let writeJsonArray
query
(parameters: SqliteParameter seq)
writer
(mapFunc: SqliteDataReader -> string)
(conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
writeJsonArray cmd writer mapFunc
/// <summary>Execute a query, writing its results to the given <c>PipeWriter</c></summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
let WriteJsonArray(query, parameters, writer, mapFunc: System.Func<SqliteDataReader, string>, conn) =
writeJsonArray query parameters writer mapFunc.Invoke conn
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns><c>Some</c> with the first matching result, or <c>None</c> if not found</returns>
[<CompiledName "FSharpSingle">]
let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) conn = backgroundTask {
let! results = list query parameters mapFunc conn
return FSharp.Collections.List.tryHead results
}
/// <summary>Execute a query that returns one or no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function between the document and the domain item</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The first matching result, or <c>null</c> if not found</returns>
let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>(
query, parameters, mapFunc: System.Func<SqliteDataReader, 'TDoc>, conn
) = backgroundTask {
let! result = single<'TDoc> query parameters mapFunc.Invoke conn
return Option.toObj result
}
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
[<CompiledName "FSharpJsonSingle">]
let jsonSingle query parameters mapFunc conn = backgroundTask {
let! results = jsonArray $"%s{query} LIMIT 1" parameters mapFunc conn
return if results = "[]" then "{}" else results[1..results.Length - 2]
}
/// <summary>Execute a query that returns one or no JSON documents</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to extract the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The JSON document with the first matching result, or an empty document if not found</returns>
let JsonSingle(query, parameters, mapFunc: System.Func<SqliteDataReader, string>, conn) =
jsonSingle query parameters mapFunc.Invoke conn
/// <summary>Execute a query that returns no results</summary>
/// <param name="query">The query to retrieve the results</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "NonQuery">]
let nonQuery query (parameters: SqliteParameter seq) (conn: SqliteConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
write cmd
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The scalar value for the query</returns>
[<CompiledName "FSharpScalar">]
let scalar<'T when 'T : struct> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'T)
(conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- query
cmd.Parameters.AddRange parameters
use! rdr = cmd.ExecuteReaderAsync()
let! isFound = rdr.ReadAsync()
return if isFound then mapFunc rdr else Unchecked.defaultof<'T>
}
/// <summary>Execute a query that returns a scalar value</summary>
/// <param name="query">The query to retrieve the value</param>
/// <param name="parameters">Parameters to use for the query</param>
/// <param name="mapFunc">The mapping function to obtain the value</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The scalar value for the query</returns>
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<SqliteDataReader, 'T>, conn) =
scalar<'T> query parameters mapFunc.Invoke conn
/// <summary>Functions to create tables and indexes</summary>
[<RequireQualifiedAccess>]
module Definition =
/// <summary>Create a document table</summary>
/// <param name="name">The table whose existence should be ensured (may include schema)</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "EnsureTable">]
let ensureTable name conn = backgroundTask {
do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn
do! Custom.nonQuery (Query.Definition.ensureKey name SQLite) [] conn
}
/// <summary>Create an index on field(s) within documents in the specified table</summary>
/// <param name="tableName">The table to be indexed (may include schema)</param>
/// <param name="indexName">The name of the index to create</param>
/// <param name="fields">One or more fields to be indexed</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn
/// <summary>Commands to add documents</summary>
[<AutoOpen>]
module Document =
/// <summary>Insert a new document</summary>
/// <param name="tableName">The table into which the document should be inserted (may include schema)</param>
/// <param name="document">The document to be inserted</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) conn =
let query =
match Configuration.autoIdStrategy () with
| Disabled -> Query.insert tableName
| strategy ->
let idField = Configuration.idField ()
let dataParam =
if AutoId.NeedsAutoId strategy document idField then
match strategy with
| Number -> $"(SELECT coalesce(max(data->>'{idField}'), 0) + 1 FROM {tableName})"
| Guid -> $"'{AutoId.GenerateGuid()}'"
| RandomString -> $"'{AutoId.GenerateRandomString(Configuration.idStringLength ())}'"
| Disabled -> "@data"
|> function it -> $"json_set(@data, '$.{idField}', {it})"
else "@data"
(Query.insert tableName).Replace("@data", dataParam)
Custom.nonQuery query [ jsonParam "@data" document ] conn
/// <summary>Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")</summary>
/// <param name="tableName">The table into which the document should be saved (may include schema)</param>
/// <param name="document">The document to be saved</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) conn =
Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn
/// <summary>Commands to count documents</summary>
[<RequireQualifiedAccess>]
module Count =
/// <summary>Count all documents in a table</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The count of the documents in the table</returns>
[<CompiledName "All">]
let all tableName conn =
Custom.scalar (Query.count tableName) [] toCount conn
/// <summary>Count matching documents using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be counted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The count of matching documents in the table</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.scalar
(Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount conn
/// <summary>Commands to determine if documents exist</summary>
[<RequireQualifiedAccess>]
module Exists =
/// <summary>Determine if a document exists for the given ID</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="docId">The ID of the document whose existence should be checked</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>True if a document exists, false if not</returns>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.scalar (Query.exists tableName (Query.whereById docId)) [ idParam docId ] toExists conn
/// <summary>Determine if a document exists using JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which existence should be checked (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>True if any matching documents exist, false if not</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.scalar
(Query.exists tableName (Query.whereByFields howMatched fields))
(addFieldParams fields [])
toExists
conn
/// <summary>Commands to retrieve documents as domain items</summary>
[<RequireQualifiedAccess>]
module Find =
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents from the given table</returns>
[<CompiledName "FSharpAll">]
let all<'TDoc> tableName conn =
Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> conn
/// <summary>Retrieve all documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents from the given table</returns>
let All<'TDoc>(tableName, conn) =
Custom.List(Query.find tableName, [], fromData<'TDoc>, conn)
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
[<CompiledName "FSharpAllOrdered">]
let allOrdered<'TDoc> tableName orderFields conn =
Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields SQLite) [] fromData<'TDoc> conn
/// <summary>Retrieve all documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents from the given table, ordered by the given fields</returns>
let AllOrdered<'TDoc>(tableName, orderFields, conn) =
Custom.List(Query.find tableName + Query.orderBy orderFields SQLite, [], fromData<'TDoc>, conn)
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns><c>Some</c> with the document if found, <c>None</c> otherwise</returns>
[<CompiledName "FSharpById">]
let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn =
Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn
/// <summary>Retrieve a document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The document if found, <c>null</c> otherwise</returns>
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)
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents matching the given fields</returns>
[<CompiledName "FSharpByFields">]
let byFields<'TDoc> tableName howMatched fields conn =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
fromData<'TDoc>
conn
/// <summary>Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents matching the given fields</returns>
let ByFields<'TDoc>(tableName, howMatched, fields, conn) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched fields,
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "FSharpByFieldsOrdered">]
let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn =
Custom.list<'TDoc>
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
fromData<'TDoc>
conn
/// <summary>
/// Retrieve documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the
/// document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All documents matching the given fields, ordered by the other given fields</returns>
let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) =
Custom.List<'TDoc>(
Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite,
addFieldParams queryFields [],
fromData<'TDoc>,
conn)
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns><c>Some</c> with the first document, or <c>None</c> if not found</returns>
[<CompiledName "FSharpFirstByFields">]
let firstByFields<'TDoc> tableName howMatched fields conn =
Custom.single
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1"
(addFieldParams fields [])
fromData<'TDoc>
conn
/// <summary>Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The first document, or <c>null</c> if not found</returns>
let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, conn) =
Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1",
addFieldParams fields [],
fromData<'TDoc>,
conn)
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>
/// <c>Some</c> with the first document ordered by the given fields, or <c>None</c> if not found
/// </returns>
[<CompiledName "FSharpFirstByFieldsOrdered">]
let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn =
Custom.single
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1"
(addFieldParams queryFields [])
fromData<'TDoc>
conn
/// <summary>
/// Retrieve the first document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The first document ordered by the given fields, or <c>null</c> if not found</returns>
let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(
tableName, howMatched, queryFields, orderFields, conn) =
Custom.Single(
$"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1",
addFieldParams queryFields [],
fromData<'TDoc>,
conn)
/// <summary>Commands to retrieve documents as raw JSON</summary>
[<RequireQualifiedAccess>]
module Json =
/// <summary>Retrieve all JSON documents in the given table</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All JSON documents from the given table</returns>
[<CompiledName "All">]
let all tableName conn =
Custom.jsonArray (Query.find tableName) [] jsonFromData conn
/// <summary>Retrieve all JSON documents in the given table ordered by the given fields in the document</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All JSON documents from the given table, ordered by the given fields</returns>
[<CompiledName "AllOrdered">]
let allOrdered tableName orderFields conn =
Custom.jsonArray (Query.find tableName + Query.orderBy orderFields SQLite) [] jsonFromData conn
/// <summary>Retrieve a JSON document by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "ById">]
let byId<'TKey> tableName (docId: 'TKey) conn =
Custom.jsonSingle (Query.byId (Query.find tableName) docId) [ idParam docId ] jsonFromData conn
/// <summary>Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All JSON documents matching the given fields</returns>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.jsonArray
(Query.byFields (Query.find tableName) howMatched fields) (addFieldParams fields []) jsonFromData conn
/// <summary>
/// Retrieve JSON documents matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields
/// in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>All JSON documents matching the given fields, ordered by the other given fields</returns>
[<CompiledName "ByFieldsOrdered">]
let byFieldsOrdered tableName howMatched queryFields orderFields conn =
Custom.jsonArray
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
jsonFromData
conn
/// <summary>Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The first JSON document if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFields">]
let firstByFields tableName howMatched fields conn =
Custom.jsonSingle
(Query.byFields (Query.find tableName) howMatched fields) (addFieldParams fields []) jsonFromData conn
/// <summary>
/// Retrieve the first JSON document matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.) ordered by the given
/// fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
/// <returns>The first JSON document (in order) if found, an empty JSON document otherwise</returns>
[<CompiledName "FirstByFieldsOrdered">]
let firstByFieldsOrdered tableName howMatched queryFields orderFields conn =
Custom.jsonSingle
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
jsonFromData
conn
/// <summary>Write all JSON documents in the given table to the given <c>PipeWriter</c></summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteAll">]
let writeAll tableName writer conn =
Custom.writeJsonArray (Query.find tableName) [] writer jsonFromData conn
/// <summary>
/// Write all JSON all documents in the given table to the given <c>PipeWriter</c>, ordered by the given fields in
/// the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteAllOrdered">]
let writeAllOrdered tableName writer orderFields conn =
Custom.writeJsonArray (Query.find tableName + Query.orderBy orderFields SQLite) [] writer jsonFromData conn
/// <summary>Write a JSON document to the given <c>PipeWriter</c> by its ID</summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="docId">The ID of the document to retrieve</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteById">]
let writeById<'TKey> tableName writer (docId: 'TKey) conn = backgroundTask {
let! json = Custom.jsonSingle (Query.byId (Query.find tableName) docId) [ idParam docId ] jsonFromData conn
let! _ = PipeWriter.writeString writer json
()
}
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteByFields">]
let writeByFields tableName writer howMatched fields conn =
Custom.writeJsonArray
(Query.byFields (Query.find tableName) howMatched fields)
(addFieldParams fields [])
writer
jsonFromData
conn
/// <summary>
/// Write JSON documents to the given <c>PipeWriter</c> matching JSON field comparisons (<c>-&gt;&gt; =</c>, etc.)
/// ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which documents should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteByFieldsOrdered">]
let writeByFieldsOrdered tableName writer howMatched queryFields orderFields conn =
Custom.writeJsonArray
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
writer
jsonFromData
conn
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteFirstByFields">]
let writeFirstByFields tableName writer howMatched fields conn = backgroundTask {
let! json =
Custom.jsonSingle
(Query.byFields (Query.find tableName) howMatched fields) (addFieldParams fields []) jsonFromData conn
let! _ = PipeWriter.writeString writer json
()
}
/// <summary>
/// Write the first JSON document to the given <c>PipeWriter</c> matching JSON field comparisons
/// (<c>-&gt;&gt; =</c>, etc.) ordered by the given fields in the document
/// </summary>
/// <param name="tableName">The table from which a document should be retrieved (may include schema)</param>
/// <param name="writer">The PipeWriter to which the results should be written</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="queryFields">The field conditions to match</param>
/// <param name="orderFields">Fields by which the results should be ordered</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "WriteFirstByFieldsOrdered">]
let writeFirstByFieldsOrdered tableName writer howMatched queryFields orderFields conn = backgroundTask {
let! json =
Custom.jsonSingle
(Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields SQLite)
(addFieldParams queryFields [])
jsonFromData
conn
let! _ = PipeWriter.writeString writer json
()
}
/// <summary>Commands to update documents</summary>
[<RequireQualifiedAccess>]
module Update =
/// <summary>Update (replace) an entire document by its ID</summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="docId">The ID of the document to be updated (replaced)</param>
/// <param name="document">The new document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (document: 'TDoc) conn =
Custom.nonQuery
(Query.statementWhere (Query.update tableName) (Query.whereById docId))
[ idParam docId; jsonParam "@data" document ]
conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "FSharpByFunc">]
let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn =
byId tableName (idFunc document) document conn
/// <summary>
/// Update (replace) an entire document by its ID, using the provided function to obtain the ID from the document
/// </summary>
/// <param name="tableName">The table in which a document should be updated (may include schema)</param>
/// <param name="idFunc">The function to obtain the ID of the document</param>
/// <param name="document">The new document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) =
byFunc tableName idFunc.Invoke document conn
/// <summary>Commands to patch (partially update) documents</summary>
[<RequireQualifiedAccess>]
module Patch =
/// <summary>Patch a document by its ID</summary>
/// <param name="tableName">The table in which a document should be patched (may include schema)</param>
/// <param name="docId">The ID of the document to patch</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) (patch: 'TPatch) conn =
Custom.nonQuery
(Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] conn
/// <summary>
/// Patch documents using a JSON field comparison query in the <c>WHERE</c> clause (<c>-&gt;&gt; =</c>, etc.)
/// </summary>
/// <param name="tableName">The table in which documents should be patched (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="patch">The partial document to patch the existing document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields (patch: 'TPatch) conn =
Custom.nonQuery
(Query.byFields (Query.patch tableName) howMatched fields)
(addFieldParams fields [ jsonParam "@data" patch ])
conn
/// <summary>Commands to remove fields from documents</summary>
[<RequireQualifiedAccess>]
module RemoveFields =
/// <summary>Remove fields from a document by the document's ID</summary>
/// <param name="tableName">The table in which a document should be modified (may include schema)</param>
/// <param name="docId">The ID of the document to modify</param>
/// <param name="fieldNames">One or more field names to remove from the document</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery
(Query.byId (Query.removeFields tableName nameParams) docId)
(idParam docId |> Seq.singleton |> Seq.append nameParams)
conn
/// <summary>Remove fields from documents via a comparison on JSON fields in the document</summary>
/// <param name="tableName">The table in which documents should be modified (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="fieldNames">One or more field names to remove from the matching documents</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields fieldNames conn =
let nameParams = fieldNameParams "@name" fieldNames
Custom.nonQuery
(Query.byFields (Query.removeFields tableName nameParams) howMatched fields)
(addFieldParams fields nameParams)
conn
/// <summary>Commands to delete documents</summary>
[<RequireQualifiedAccess>]
module Delete =
/// <summary>Delete a document by its ID</summary>
/// <param name="tableName">The table in which a document should be deleted (may include schema)</param>
/// <param name="docId">The ID of the document to delete</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ById">]
let byId tableName (docId: 'TKey) conn =
Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] conn
/// <summary>Delete documents by matching a JSON field comparison query (<c>-&gt;&gt; =</c>, etc.)</summary>
/// <param name="tableName">The table in which documents should be deleted (may include schema)</param>
/// <param name="howMatched">Whether to match any or all of the field conditions</param>
/// <param name="fields">The field conditions to match</param>
/// <param name="conn">The <c>SqliteConnection</c> to use to execute the query</param>
[<CompiledName "ByFields">]
let byFields tableName howMatched fields conn =
Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn

View File

@ -3,8 +3,6 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,6 @@
using System.IO.Pipelines;
using Expecto.CSharp;
using Expecto;
using Microsoft.FSharp.Core;
using Microsoft.FSharp.Collections;
namespace BitBadger.Documents.Tests.CSharp;
@ -17,413 +16,19 @@ internal class TestSerializer : IDocumentSerializer
}
/// <summary>
/// C# Tests for common functionality in <c>BitBadger.Documents</c>
/// C# Tests for common functionality in <tt>BitBadger.Documents</tt>
/// </summary>
public static class CommonCSharpTests
{
/// <summary>
/// Unit tests for the OpSql property of the Comparison discriminated union
/// Unit tests
/// </summary>
private static readonly Test OpTests = TestList("Comparison.OpSql",
[
TestCase("Equal succeeds", () =>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit", new[]
{
Expect.equal(Comparison.NewEqual("").OpSql, "=", "The Equals SQL was not correct");
}),
TestCase("Greater succeeds", () =>
TestSequenced(
TestList("Configuration", new[]
{
Expect.equal(Comparison.NewGreater("").OpSql, ">", "The Greater SQL was not correct");
}),
TestCase("GreaterOrEqual succeeds", () =>
{
Expect.equal(Comparison.NewGreaterOrEqual("").OpSql, ">=", "The GreaterOrEqual SQL was not correct");
}),
TestCase("Less succeeds", () =>
{
Expect.equal(Comparison.NewLess("").OpSql, "<", "The Less SQL was not correct");
}),
TestCase("LessOrEqual succeeds", () =>
{
Expect.equal(Comparison.NewLessOrEqual("").OpSql, "<=", "The LessOrEqual SQL was not correct");
}),
TestCase("NotEqual succeeds", () =>
{
Expect.equal(Comparison.NewNotEqual("").OpSql, "<>", "The NotEqual SQL was not correct");
}),
TestCase("Between succeeds", () =>
{
Expect.equal(Comparison.NewBetween("", "").OpSql, "BETWEEN", "The Between SQL was not correct");
}),
TestCase("In succeeds", () =>
{
Expect.equal(Comparison.NewIn([]).OpSql, "IN", "The In SQL was not correct");
}),
TestCase("InArray succeeds", () =>
{
Expect.equal(Comparison.NewInArray("", []).OpSql, "?|", "The InArray SQL was not correct");
}),
TestCase("Exists succeeds", () =>
{
Expect.equal(Comparison.Exists.OpSql, "IS NOT NULL", "The Exists SQL was not correct");
}),
TestCase("NotExists succeeds", () =>
{
Expect.equal(Comparison.NotExists.OpSql, "IS NULL", "The NotExists SQL was not correct");
})
]);
/// <summary>
/// Unit tests for the Field class
/// </summary>
private static readonly Test FieldTests = TestList("Field",
[
TestCase("Equal succeeds", () =>
{
var field = Field.Equal("Test", 14);
Expect.equal(field.Name, "Test", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewEqual(14), "Comparison incorrect");
}),
TestCase("Greater succeeds", () =>
{
var field = Field.Greater("Great", "night");
Expect.equal(field.Name, "Great", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewGreater("night"), "Comparison incorrect");
}),
TestCase("GreaterOrEqual succeeds", () =>
{
var field = Field.GreaterOrEqual("Nice", 88L);
Expect.equal(field.Name, "Nice", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewGreaterOrEqual(88L), "Comparison incorrect");
}),
TestCase("Less succeeds", () =>
{
var field = Field.Less("Lesser", "seven");
Expect.equal(field.Name, "Lesser", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewLess("seven"), "Comparison incorrect");
}),
TestCase("LessOrEqual succeeds", () =>
{
var field = Field.LessOrEqual("Nobody", "KNOWS");
Expect.equal(field.Name, "Nobody", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewLessOrEqual("KNOWS"), "Comparison incorrect");
}),
TestCase("NotEqual succeeds", () =>
{
var field = Field.NotEqual("Park", "here");
Expect.equal(field.Name, "Park", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewNotEqual("here"), "Comparison incorrect");
}),
TestCase("Between succeeds", () =>
{
var field = Field.Between("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Comparison, Comparison.NewBetween(18, 49), "Comparison incorrect");
}),
TestCase("In succeeds", () =>
{
var field = Field.In("Here", [8, 16, 32]);
Expect.equal(field.Name, "Here", "Field name incorrect");
Expect.isTrue(field.Comparison.IsIn, "Comparison incorrect");
Expect.sequenceEqual(((Comparison.In)field.Comparison).Values, [8, 16, 32], "Value incorrect");
}),
TestCase("InArray succeeds", () =>
{
var field = Field.InArray("ArrayField", "table", ["x", "y", "z"]);
Expect.equal(field.Name, "ArrayField", "Field name incorrect");
Expect.isTrue(field.Comparison.IsInArray, "Comparison incorrect");
var it = (Comparison.InArray)field.Comparison;
Expect.equal(it.Table, "table", "Table name incorrect");
Expect.sequenceEqual(it.Values, ["x", "y", "z"], "Value incorrect");
}),
TestCase("Exists succeeds", () =>
{
var field = Field.Exists("Groovy");
Expect.equal(field.Name, "Groovy", "Field name incorrect");
Expect.isTrue(field.Comparison.IsExists, "Comparison incorrect");
}),
TestCase("NotExists succeeds", () =>
{
var field = Field.NotExists("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.isTrue(field.Comparison.IsNotExists, "Comparison incorrect");
}),
TestList("NameToPath",
[
TestCase("succeeds for PostgreSQL and a simple name", () =>
{
Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL, FieldFormat.AsSql),
"Path not constructed correctly");
}),
TestCase("succeeds for SQLite and a simple name", () =>
{
Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite, FieldFormat.AsSql),
"Path not constructed correctly");
}),
TestCase("succeeds for PostgreSQL and a nested name", () =>
{
Expect.equal("data#>>'{A,Long,Path,to,the,Property}'",
Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL, FieldFormat.AsSql),
"Path not constructed correctly");
}),
TestCase("succeeds for SQLite and a nested name", () =>
{
Expect.equal("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite, FieldFormat.AsSql),
"Path not constructed correctly");
})
]),
TestCase("WithParameterName succeeds", () =>
{
var field = Field.Equal("Bob", "Tom").WithParameterName("@name");
Expect.isSome(field.ParameterName, "The parameter name should have been filled");
Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect");
}),
TestCase("WithQualifier succeeds", () =>
{
var field = Field.Equal("Bill", "Matt").WithQualifier("joe");
Expect.isSome(field.Qualifier, "The table qualifier should have been filled");
Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect");
}),
TestList("Path",
[
TestCase("succeeds for a PostgreSQL single field with no qualifier", () =>
{
var field = Field.GreaterOrEqual("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL single field with a qualifier", () =>
{
var field = Field.Less("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with no qualifier", () =>
{
var field = Field.Equal("My.Nested.Field", "howdy");
Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a PostgreSQL nested field with a qualifier", () =>
{
var field = Field.Equal("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL, FieldFormat.AsSql),
"The PostgreSQL path is incorrect");
}),
TestCase("succeeds for a SQLite single field with no qualifier", () =>
{
var field = Field.GreaterOrEqual("SomethingCool", 18);
Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite single field with a qualifier", () =>
{
var field = Field.Less("SomethingElse", 9).WithQualifier("this");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with no qualifier", () =>
{
var field = Field.Equal("My.Nested.Field", "howdy");
Expect.equal("data->'My'->'Nested'->>'Field'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with a qualifier", () =>
{
var field = Field.Equal("Nest.Away", "doc").WithQualifier("bird");
Expect.equal("bird.data->'Nest'->>'Away'", field.Path(Dialect.SQLite, FieldFormat.AsSql),
"The SQLite path is incorrect");
})
])
]);
/// <summary>
/// Unit tests for the FieldMatch enum
/// </summary>
private static readonly Test FieldMatchTests = TestList("FieldMatch.ToString",
[
TestCase("succeeds for Any", () =>
{
Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect");
}),
TestCase("succeeds for All", () =>
{
Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect");
})
]);
/// <summary>
/// Unit tests for the ParameterName class
/// </summary>
private static readonly Test ParameterNameTests = TestList("ParameterName.Derive",
[
TestCase("succeeds with existing name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.Some("@taco")), "@taco", "Name should have been @taco");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Counter should not have advanced for named field");
}),
TestCase("Derive succeeds with non-existent name", () =>
{
ParameterName name = new();
Expect.equal(name.Derive(FSharpOption<string>.None), "@field0",
"Anonymous field name should have been returned");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field1",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field2",
"Counter should have advanced from previous call");
Expect.equal(name.Derive(FSharpOption<string>.None), "@field3",
"Counter should have advanced from previous call");
})
]);
/// <summary>
/// Unit tests for the AutoId enum
/// </summary>
private static readonly Test AutoIdTests = TestList("AutoId",
[
TestCase("GenerateGuid succeeds", () =>
{
var autoId = AutoId.GenerateGuid();
Expect.isNotNull(autoId, "The GUID auto-ID should not have been null");
Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long");
Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase");
}),
TestCase("GenerateRandomString succeeds", () =>
{
foreach (var length in (int[]) [6, 8, 12, 20, 32, 57, 64])
{
var autoId = AutoId.GenerateRandomString(length);
Expect.isNotNull(autoId, $"Random string ({length}) should not have been null");
Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long");
Expect.equal(autoId, autoId.ToLowerInvariant(),
$"Random string ({length}) should have been lowercase");
}
}),
TestList("NeedsAutoId",
[
TestCase("succeeds when no auto ID is configured", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Disabled, new object(), "id"),
"Disabled auto-ID never needs an automatic ID");
}),
TestCase("fails for any when the ID property is not found", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.Number, new { Key = "" }, "Id");
Expect.isTrue(false, "Non-existent ID property should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
}),
TestCase("succeeds for byte when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)0 }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for byte when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)4 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for short when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)0 }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for short when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)7 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for int when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0 }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for int when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 32 }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("succeeds for long when the ID is zero", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0L }, "Id"),
"Zero ID should have returned true");
}),
TestCase("succeeds for long when the ID is non-zero", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 80L }, "Id"),
"Non-zero ID should have returned false");
}),
TestCase("fails for number when the ID is not a number", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.Number, new { Id = "" }, "Id");
Expect.isTrue(false, "Numeric ID against a string should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
}),
TestCase("succeeds for GUID when the ID is blank", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "" }, "Id"),
"Blank ID should have returned true");
}),
TestCase("succeeds for GUID when the ID is filled", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "abc" }, "Id"),
"Filled ID should have returned false");
}),
TestCase("fails for GUID when the ID is not a string", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.Guid, new { Id = 8 }, "Id");
Expect.isTrue(false, "String ID against a number should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
}),
TestCase("succeeds for RandomString when the ID is blank", () =>
{
Expect.isTrue(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "" }, "Id"),
"Blank ID should have returned true");
}),
TestCase("succeeds for RandomString when the ID is filled", () =>
{
Expect.isFalse(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "x" }, "Id"),
"Filled ID should have returned false");
}),
TestCase("fails for RandomString when the ID is not a string", () =>
{
try
{
_ = AutoId.NeedsAutoId(AutoId.RandomString, new { Id = 33 }, "Id");
Expect.isTrue(false, "String ID against a number should have thrown an exception");
}
catch (InvalidOperationException)
{
// pass
}
})
])
]);
/// <summary>
/// Unit tests for the Configuration static class
/// </summary>
private static readonly Test ConfigurationTests = TestList("Configuration",
[
TestCase("UseSerializer succeeds", () =>
{
try
@ -464,96 +69,151 @@ public static class CommonCSharpTests
{
Configuration.UseIdField("Id");
}
}),
TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () =>
{
try
{
Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled,
"The default auto-ID strategy was incorrect");
Configuration.UseAutoIdStrategy(AutoId.Guid);
Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid,
"The auto-ID strategy was not set correctly");
}
finally
{
Configuration.UseAutoIdStrategy(AutoId.Disabled);
}
}),
TestCase("UseIdStringLength / IdStringLength succeeds", () =>
{
try
{
Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect");
Configuration.UseIdStringLength(33);
Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly");
}
finally
{
Configuration.UseIdStringLength(16);
}
})
]);
/// <summary>
/// Unit tests for the Query static class
/// </summary>
private static readonly Test QueryTests = TestList("Query",
[
TestCase("StatementWhere succeeds", () =>
})),
TestList("Op", new[]
{
Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly");
TestCase("EQ succeeds", () =>
{
Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct");
}),
TestList("Definition",
[
TestCase("GT succeeds", () =>
{
Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct");
}),
TestCase("GE succeeds", () =>
{
Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct");
}),
TestCase("LT succeeds", () =>
{
Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct");
}),
TestCase("LE succeeds", () =>
{
Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct");
}),
TestCase("NE succeeds", () =>
{
Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct");
}),
TestCase("BT succeeds", () =>
{
Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct");
}),
TestCase("EX succeeds", () =>
{
Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct");
}),
TestCase("NEX succeeds", () =>
{
Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct");
})
}),
TestList("Field", new[]
{
TestCase("EQ succeeds", () =>
{
var field = Field.EQ("Test", 14);
Expect.equal(field.Name, "Test", "Field name incorrect");
Expect.equal(field.Op, Op.EQ, "Operator incorrect");
Expect.equal(field.Value, 14, "Value incorrect");
}),
TestCase("GT succeeds", () =>
{
var field = Field.GT("Great", "night");
Expect.equal(field.Name, "Great", "Field name incorrect");
Expect.equal(field.Op, Op.GT, "Operator incorrect");
Expect.equal(field.Value, "night", "Value incorrect");
}),
TestCase("GE succeeds", () =>
{
var field = Field.GE("Nice", 88L);
Expect.equal(field.Name, "Nice", "Field name incorrect");
Expect.equal(field.Op, Op.GE, "Operator incorrect");
Expect.equal(field.Value, 88L, "Value incorrect");
}),
TestCase("LT succeeds", () =>
{
var field = Field.LT("Lesser", "seven");
Expect.equal(field.Name, "Lesser", "Field name incorrect");
Expect.equal(field.Op, Op.LT, "Operator incorrect");
Expect.equal(field.Value, "seven", "Value incorrect");
}),
TestCase("LE succeeds", () =>
{
var field = Field.LE("Nobody", "KNOWS");
Expect.equal(field.Name, "Nobody", "Field name incorrect");
Expect.equal(field.Op, Op.LE, "Operator incorrect");
Expect.equal(field.Value, "KNOWS", "Value incorrect");
}),
TestCase("NE succeeds", () =>
{
var field = Field.NE("Park", "here");
Expect.equal(field.Name, "Park", "Field name incorrect");
Expect.equal(field.Op, Op.NE, "Operator incorrect");
Expect.equal(field.Value, "here", "Value incorrect");
}),
TestCase("BT succeeds", () =>
{
var field = Field.BT("Age", 18, 49);
Expect.equal(field.Name, "Age", "Field name incorrect");
Expect.equal(field.Op, Op.BT, "Operator incorrect");
Expect.equal(((FSharpList<object>)field.Value).ToArray(), new object[] { 18, 49 }, "Value incorrect");
}),
TestCase("EX succeeds", () =>
{
var field = Field.EX("Groovy");
Expect.equal(field.Name, "Groovy", "Field name incorrect");
Expect.equal(field.Op, Op.EX, "Operator incorrect");
}),
TestCase("NEX succeeds", () =>
{
var field = Field.NEX("Rad");
Expect.equal(field.Name, "Rad", "Field name incorrect");
Expect.equal(field.Op, Op.NEX, "Operator incorrect");
})
}),
TestList("Query", new[]
{
TestCase("SelectFromTable succeeds", () =>
{
Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table",
"SELECT statement not correct");
}),
TestList("Definition", new[]
{
TestCase("EnsureTableFor succeeds", () =>
{
Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"),
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
"CREATE TABLE statement not constructed correctly");
}),
TestList("EnsureKey",
[
TestList("EnsureKey", new[]
{
TestCase("succeeds when a schema is present", () =>
{
Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))",
Expect.equal(Query.Definition.EnsureKey("test.table"),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))",
"CREATE INDEX for key statement with schema not constructed correctly");
}),
TestCase("succeeds when a schema is not present", () =>
{
Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))",
Expect.equal(Query.Definition.EnsureKey("table"),
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))",
"CREATE INDEX for key statement without schema not constructed correctly");
})
]),
TestList("EnsureIndexOn",
[
TestCase("succeeds for multiple fields and directions", () =>
}),
TestCase("EnsureIndexOn succeeds for multiple fields and directions", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("test.table", "gibberish",
["taco", "guac DESC", "salsa ASC"], Dialect.SQLite),
new[] { "taco", "guac DESC", "salsa ASC" }),
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
+ "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
+ "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)",
"CREATE INDEX for multiple field statement incorrect");
}),
TestCase("succeeds for nested PostgreSQL field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))",
"CREATE INDEX for nested PostgreSQL field incorrect");
}),
TestCase("succeeds for nested SQLite field", () =>
{
Expect.equal(
Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite),
"CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->'a'->'b'->>'c'))",
"CREATE INDEX for nested SQLite field incorrect");
})
])
]),
}),
TestCase("Insert succeeds", () =>
{
Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct");
@ -563,213 +223,7 @@ public static class CommonCSharpTests
Expect.equal(Query.Save("tbl"),
"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data",
"INSERT ON CONFLICT UPDATE statement not correct");
}),
TestCase("Count succeeds", () =>
{
Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct");
}),
TestCase("Exists succeeds", () =>
{
Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it",
"Exists query not correct");
}),
TestCase("Find succeeds", () =>
{
Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct");
}),
TestCase("Update succeeds", () =>
{
Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct");
}),
TestCase("Delete succeeds", () =>
{
Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct");
}),
TestList("OrderBy",
[
TestCase("succeeds for no fields", () =>
{
Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)");
Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)");
}),
TestCase("succeeds for PostgreSQL with one field and no direction", () =>
{
Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL),
" ORDER BY data->>'TestField'", "Order By not constructed correctly");
}),
TestCase("succeeds for SQLite with one field and no direction", () =>
{
Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite),
" ORDER BY data->>'TestField'", "Order By not constructed correctly");
}),
TestCase("succeeds for PostgreSQL with multiple fields and direction", () =>
{
Expect.equal(
Query.OrderBy(
[
Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"),
Field.Named("It DESC")
], Dialect.PostgreSQL),
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
TestCase("succeeds for SQLite with multiple fields and direction", () =>
{
Expect.equal(
Query.OrderBy(
[
Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"),
Field.Named("It DESC")
], Dialect.SQLite),
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
TestCase("succeeds for PostgreSQL numeric fields", () =>
{
Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL),
" ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field");
}),
TestCase("succeeds for SQLite numeric fields", () =>
{
Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'",
"Order By not constructed correctly for numeric field");
}),
TestCase("succeeds for PostgreSQL case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field DESC NULLS FIRST")], Dialect.PostgreSQL),
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
"Order By not constructed correctly for case-insensitive field");
}),
TestCase("succeeds for SQLite case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field ASC NULLS LAST")], Dialect.SQLite),
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
"Order By not constructed correctly for case-insensitive field");
})
])
]);
private static string StreamText(Stream stream)
{
stream.Position = 0L;
using StreamReader reader = new(stream);
return reader.ReadToEnd();
}
/// <summary>Unit tests for the PipeWriter module</summary>
private static readonly Test PipeWriterTests = TestList("PipeWriterModule",
[
TestList("WriteString",
[
TestCase("succeeds when writer is open", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
try
{
var result = await PipeWriterModule.WriteString(writer, "abc");
Expect.isTrue(result, "The write operation should have been successful");
Expect.equal(StreamText(stream), "abc", "The string was not written correctly");
}
finally
{
await writer.CompleteAsync();
}
}),
TestCase("succeeds when writer is completed", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
await writer.CompleteAsync();
var result = await PipeWriterModule.WriteString(writer, "abc");
Expect.isFalse(result, "The write operation should have returned false");
Expect.equal(StreamText(stream), "", "No text should have been written");
})
]),
TestList("WriteStrings",
[
TestCase("succeeds with no strings", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
try
{
await PipeWriterModule.WriteStrings(writer, []);
Expect.equal(StreamText(stream), "[]", "An empty sequence of strings was not written correctly");
}
finally
{
await writer.CompleteAsync();
}
}),
TestCase("succeeds with one strings", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
try
{
await PipeWriterModule.WriteStrings(writer, ["le-test"]);
Expect.equal(StreamText(stream), "[le-test]", "A sequence of one string was not written correctly");
}
finally
{
await writer.CompleteAsync();
}
}),
TestCase("succeeds with many strings", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
try
{
await PipeWriterModule.WriteStrings(writer, ["z", "y", "x", "c", "b", "a"]);
Expect.equal(StreamText(stream), "[z,y,x,c,b,a]",
"A sequence of many strings was not written correctly");
}
finally
{
await writer.CompleteAsync();
}
}),
TestCase("succeeds when the writer is completed early", async () =>
{
await using MemoryStream stream = new();
var writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(leaveOpen: true));
await PipeWriterModule.WriteStrings(writer, Items());
Expect.equal(StreamText(stream), "[a,b,c", "The writing should have stopped when the writer completed");
return;
IEnumerable<string> Items()
{
yield return "a";
yield return "b";
yield return "c";
writer.Complete();
yield return "d";
yield return "e";
yield return "f";
}
})
])
]);
/// <summary>
/// Unit tests
/// </summary>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit",
[
OpTests,
FieldTests,
FieldMatchTests,
ParameterNameTests,
AutoIdTests,
QueryTests,
PipeWriterTests,
TestSequenced(ConfigurationTests)
]);
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -131,7 +131,7 @@ public static class PostgresDb
var sqlProps = Sql.connect(database.ConnectionString);
Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps));
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName, Dialect.PostgreSQL), sqlProps));
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps));
Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,5 @@
namespace BitBadger.Documents.Tests.CSharp;
public class NumIdDocument
{
public int Key { get; set; } = 0;
public string Text { get; set; } = "";
}
public class SubDocument
{
public string Foo { get; set; } = "";
@ -18,48 +12,4 @@ public class JsonDocument
public string Value { get; set; } = "";
public int NumValue { get; set; } = 0;
public SubDocument? Sub { get; set; } = null;
/// <summary>
/// A set of documents used for integration tests
/// </summary>
public static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } },
new() { Id = "three", Value = "", NumValue = 4 },
new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } },
new() { Id = "five", Value = "purple", NumValue = 18 }
];
/// <summary>The JSON for document ID `one`</summary>
public static string One = """{"Id":"one","Value":"FIRST!","NumValue":0,"Sub":null}""";
/// <summary>The JSON for document ID `two`</summary>
public static string Two = """{"Id":"two","Value":"another","NumValue":10,"Sub":{"Foo":"green","Bar":"blue"}}""";
/// <summary>The JSON for document ID `three`</summary>
public static string Three = """{"Id":"three","Value":"","NumValue":4,"Sub":null}""";
/// <summary>The JSON for document ID `four`</summary>
public static string Four = """{"Id":"four","Value":"purple","NumValue":17,"Sub":{"Foo":"green","Bar":"red"}}""";
/// <summary>The JSON for document ID `five`</summary>
public static string Five = """{"Id":"five","Value":"purple","NumValue":18,"Sub":null}""";
}
public class ArrayDocument
{
public string Id { get; set; } = "";
public string[] Values { get; set; } = [];
/// <summary>
/// A set of documents used for integration tests
/// </summary>
public static readonly List<ArrayDocument> TestDocuments =
[
new() { Id = "first", Values = ["a", "b", "c"] },
new() { Id = "second", Values = ["c", "d", "e"] },
new() { Id = "third", Values = ["x", "y", "z"] }
];
}

View File

@ -2,12 +2,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<NoWarn>1182</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="CommonTests.fs" />
<Compile Include="Types.fs" />
<Compile Include="PostgresTests.fs" />
<Compile Include="PostgresExtensionTests.fs" />
<Compile Include="SqliteTests.fs" />
@ -17,7 +16,7 @@
<ItemGroup>
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>

View File

@ -1,358 +1,100 @@
module CommonTests
open System.IO
open System.IO.Pipelines
open BitBadger.Documents
open Expecto
/// Test table name
let tbl = "test_table"
/// Unit tests for the Op DU
let comparisonTests = testList "Comparison.OpSql" [
test "Equal succeeds" {
Expect.equal (Equal "").OpSql "=" "The Equals SQL was not correct"
/// Tests which do not hit the database
let all =
testList "Common" [
testList "Op" [
test "EQ succeeds" {
Expect.equal (string EQ) "=" "The equals operator was not correct"
}
test "Greater succeeds" {
Expect.equal (Greater "").OpSql ">" "The Greater SQL was not correct"
test "GT succeeds" {
Expect.equal (string GT) ">" "The greater than operator was not correct"
}
test "GreaterOrEqual succeeds" {
Expect.equal (GreaterOrEqual "").OpSql ">=" "The GreaterOrEqual SQL was not correct"
test "GE succeeds" {
Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct"
}
test "Less succeeds" {
Expect.equal (Less "").OpSql "<" "The Less SQL was not correct"
test "LT succeeds" {
Expect.equal (string LT) "<" "The less than operator was not correct"
}
test "LessOrEqual succeeds" {
Expect.equal (LessOrEqual "").OpSql "<=" "The LessOrEqual SQL was not correct"
test "LE succeeds" {
Expect.equal (string LE) "<=" "The less than or equal to operator was not correct"
}
test "NotEqual succeeds" {
Expect.equal (NotEqual "").OpSql "<>" "The NotEqual SQL was not correct"
test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct"
}
test "Between succeeds" {
Expect.equal (Between("", "")).OpSql "BETWEEN" "The Between SQL was not correct"
test "BT succeeds" {
Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct"""
}
test "In succeeds" {
Expect.equal (In []).OpSql "IN" "The In SQL was not correct"
test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct"""
}
test "InArray succeeds" {
Expect.equal (InArray("", [])).OpSql "?|" "The InArray SQL was not correct"
test "NEX succeeds" {
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct"""
}
test "Exists succeeds" {
Expect.equal Exists.OpSql "IS NOT NULL" "The Exists SQL was not correct"
}
test "NotExists succeeds" {
Expect.equal NotExists.OpSql "IS NULL" "The NotExists SQL was not correct"
}
]
/// Unit tests for the Field class
let fieldTests = testList "Field" [
test "Equal succeeds" {
let field = Field.Equal "Test" 14
]
testList "Field" [
test "EQ succeeds" {
let field = Field.EQ "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
Expect.equal field.Comparison (Equal 14) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op EQ "Operator incorrect"
Expect.equal field.Value 14 "Value incorrect"
}
test "Greater succeeds" {
let field = Field.Greater "Great" "night"
test "GT succeeds" {
let field = Field.GT "Great" "night"
Expect.equal field.Name "Great" "Field name incorrect"
Expect.equal field.Comparison (Greater "night") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op GT "Operator incorrect"
Expect.equal field.Value "night" "Value incorrect"
}
test "GreaterOrEqual succeeds" {
let field = Field.GreaterOrEqual "Nice" 88L
test "GE succeeds" {
let field = Field.GE "Nice" 88L
Expect.equal field.Name "Nice" "Field name incorrect"
Expect.equal field.Comparison (GreaterOrEqual 88L) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op GE "Operator incorrect"
Expect.equal field.Value 88L "Value incorrect"
}
test "Less succeeds" {
let field = Field.Less "Lesser" "seven"
test "LT succeeds" {
let field = Field.LT "Lesser" "seven"
Expect.equal field.Name "Lesser" "Field name incorrect"
Expect.equal field.Comparison (Less "seven") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op LT "Operator incorrect"
Expect.equal field.Value "seven" "Value incorrect"
}
test "LessOrEqual succeeds" {
let field = Field.LessOrEqual "Nobody" "KNOWS";
test "LE succeeds" {
let field = Field.LE "Nobody" "KNOWS";
Expect.equal field.Name "Nobody" "Field name incorrect"
Expect.equal field.Comparison (LessOrEqual "KNOWS") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op LE "Operator incorrect"
Expect.equal field.Value "KNOWS" "Value incorrect"
}
test "NotEqual succeeds" {
let field = Field.NotEqual "Park" "here"
test "NE succeeds" {
let field = Field.NE "Park" "here"
Expect.equal field.Name "Park" "Field name incorrect"
Expect.equal field.Comparison (NotEqual "here") "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op NE "Operator incorrect"
Expect.equal field.Value "here" "Value incorrect"
}
test "Between succeeds" {
let field = Field.Between "Age" 18 49
test "BT succeeds" {
let field = Field.BT "Age" 18 49
Expect.equal field.Name "Age" "Field name incorrect"
Expect.equal field.Comparison (Between(18, 49)) "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op BT "Operator incorrect"
Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect"
}
test "In succeeds" {
let field = Field.In "Here" [| 8; 16; 32 |]
Expect.equal field.Name "Here" "Field name 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.Qualifier "The default table qualifier should be None"
}
test "InArray succeeds" {
let field = Field.InArray "ArrayField" "table" [| "z" |]
Expect.equal field.Name "ArrayField" "Field name 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.Qualifier "The default table qualifier should be None"
}
test "Exists succeeds" {
let field = Field.Exists "Groovy"
test "EX succeeds" {
let field = Field.EX "Groovy"
Expect.equal field.Name "Groovy" "Field name incorrect"
Expect.equal field.Comparison Exists "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
Expect.equal field.Op EX "Operator incorrect"
}
test "NotExists succeeds" {
let field = Field.NotExists "Rad"
test "NEX succeeds" {
let field = Field.NEX "Rad"
Expect.equal field.Name "Rad" "Field name incorrect"
Expect.equal field.Comparison NotExists "Comparison incorrect"
Expect.isNone field.ParameterName "The default parameter name should be None"
Expect.isNone field.Qualifier "The default table qualifier should be None"
}
testList "NameToPath" [
test "succeeds for PostgreSQL and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL AsSql) "Path not constructed correctly"
}
test "succeeds for SQLite and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" SQLite AsSql) "Path not constructed correctly"
}
test "succeeds for PostgreSQL and a nested name" {
Expect.equal
"data#>>'{A,Long,Path,to,the,Property}'"
(Field.NameToPath "A.Long.Path.to.the.Property" PostgreSQL AsSql)
"Path not constructed correctly"
}
test "succeeds for SQLite and a nested name" {
Expect.equal
"data->'A'->'Long'->'Path'->'to'->'the'->>'Property'"
(Field.NameToPath "A.Long.Path.to.the.Property" SQLite AsSql)
"Path not constructed correctly"
Expect.equal field.Op NEX "Operator incorrect"
}
]
test "WithParameterName succeeds" {
let field = (Field.Equal "Bob" "Tom").WithParameterName "@name"
Expect.isSome field.ParameterName "The parameter name should have been filled"
Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect"
}
test "WithQualifier succeeds" {
let field = (Field.Equal "Bill" "Matt").WithQualifier "joe"
Expect.isSome field.Qualifier "The table qualifier should have been filled"
Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect"
}
testList "Path" [
test "succeeds for a PostgreSQL single field with no qualifier" {
let field = Field.GreaterOrEqual "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL single field with a qualifier" {
let field = { Field.Less "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with no qualifier" {
let field = Field.Equal "My.Nested.Field" "howdy"
Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with a qualifier" {
let field = { Field.Equal "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL AsSql) "The PostgreSQL path is incorrect"
}
test "succeeds for a SQLite single field with no qualifier" {
let field = Field.GreaterOrEqual "SomethingCool" 18
Expect.equal "data->>'SomethingCool'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite single field with a qualifier" {
let field = { Field.Less "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with no qualifier" {
let field = Field.Equal "My.Nested.Field" "howdy"
Expect.equal "data->'My'->'Nested'->>'Field'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
test "succeeds for a SQLite nested field with a qualifier" {
let field = { Field.Equal "Nest.Away" "doc" with Qualifier = Some "bird" }
Expect.equal "bird.data->'Nest'->>'Away'" (field.Path SQLite AsSql) "The SQLite path is incorrect"
}
]
]
/// Unit tests for the FieldMatch DU
let fieldMatchTests = testList "FieldMatch.ToString" [
test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
/// Unit tests for the ParameterName class
let parameterNameTests = testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
Expect.equal (name.Derive None) "@field0" "Counter should not have advanced for named field"
}
test "succeeds with non-existent name" {
let name = ParameterName()
Expect.equal (name.Derive None) "@field0" "Anonymous field name should have been returned"
Expect.equal (name.Derive None) "@field1" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
/// Unit tests for the AutoId DU
let autoIdTests = testList "AutoId" [
test "GenerateGuid succeeds" {
let autoId = AutoId.GenerateGuid()
Expect.isNotNull autoId "The GUID auto-ID should not have been null"
Expect.stringHasLength autoId 32 "The GUID auto-ID should have been 32 characters long"
Expect.equal autoId (autoId.ToLowerInvariant ()) "The GUID auto-ID should have been lowercase"
}
test "GenerateRandomString succeeds" {
[ 6; 8; 12; 20; 32; 57; 64 ]
|> List.iter (fun length ->
let autoId = AutoId.GenerateRandomString length
Expect.isNotNull autoId $"Random string ({length}) should not have been null"
Expect.stringHasLength autoId length $"Random string should have been {length} characters long"
Expect.equal autoId (autoId.ToLowerInvariant ()) $"Random string ({length}) should have been lowercase")
}
testList "NeedsAutoId" [
test "succeeds when no auto ID is configured" {
Expect.isFalse (AutoId.NeedsAutoId Disabled (obj ()) "id") "Disabled auto-ID never needs an automatic ID"
}
test "fails for any when the ID property is not found" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Number {| Key = "" |} "Id" |> ignore)
"Non-existent ID property should have thrown an exception"
}
test "succeeds for byte when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int8 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for byte when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int8 4 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for short when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int16 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for short when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int16 7 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for int when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0 |} "Id") "Zero ID should have returned true"
}
test "succeeds for int when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 32 |} "Id") "Non-zero ID should have returned false"
}
test "succeeds for long when the ID is zero" {
Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0L |} "Id") "Zero ID should have returned true"
}
test "succeeds for long when the ID is non-zero" {
Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 80L |} "Id") "Non-zero ID should have returned false"
}
test "fails for number when the ID is not a number" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Number {| Id = "" |} "Id" |> ignore)
"Numeric ID against a string should have thrown an exception"
}
test "succeeds for GUID when the ID is blank" {
Expect.isTrue (AutoId.NeedsAutoId Guid {| Id = "" |} "Id") "Blank ID should have returned true"
}
test "succeeds for GUID when the ID is filled" {
Expect.isFalse (AutoId.NeedsAutoId Guid {| Id = "abc" |} "Id") "Filled ID should have returned false"
}
test "fails for GUID when the ID is not a string" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId Guid {| Id = 8 |} "Id" |> ignore)
"String ID against a number should have thrown an exception"
}
test "succeeds for RandomString when the ID is blank" {
Expect.isTrue (AutoId.NeedsAutoId RandomString {| Id = "" |} "Id") "Blank ID should have returned true"
}
test "succeeds for RandomString when the ID is filled" {
Expect.isFalse (AutoId.NeedsAutoId RandomString {| Id = "x" |} "Id") "Filled ID should have returned false"
}
test "fails for RandomString when the ID is not a string" {
Expect.throwsT<System.InvalidOperationException>
(fun () -> AutoId.NeedsAutoId RandomString {| Id = 33 |} "Id" |> ignore)
"String ID against a number should have thrown an exception"
}
]
]
/// Unit tests for the Configuration module
let configurationTests = testList "Configuration" [
test "useSerializer succeeds" {
try
Configuration.useSerializer
{ new IDocumentSerializer with
member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}"""
member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T>
}
let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |}
Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used"
let deserialized = Configuration.serializer().Deserialize<obj> """{"Something":"here"}"""
Expect.isNull deserialized "Specified serializer should have returned null"
finally
Configuration.useSerializer DocumentSerializer.``default``
}
test "serializer returns configured serializer" {
Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ()))
"Serializer should have been the same"
}
test "useIdField / idField succeeds" {
try
Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect"
Configuration.useIdField "id"
Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field"
finally
Configuration.useIdField "Id"
}
test "useAutoIdStrategy / autoIdStrategy succeeds" {
try
Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect"
Configuration.useAutoIdStrategy Guid
Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly"
finally
Configuration.useAutoIdStrategy Disabled
}
test "useIdStringLength / idStringLength succeeds" {
try
Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect"
Configuration.useIdStringLength 33
Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly"
finally
Configuration.useIdStringLength 16
}
]
/// Unit tests for the Query module
let queryTests = testList "Query" [
test "statementWhere succeeds" {
Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly"
testList "Query" [
test "selectFromTable succeeds" {
Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct"
}
testList "Definition" [
test "ensureTableFor succeeds" {
@ -364,40 +106,25 @@ let queryTests = testList "Query" [
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table" PostgreSQL)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))"
(Query.Definition.ensureKey "test.table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table" SQLite)
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))"
(Query.Definition.ensureKey "table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
testList "ensureIndexOn" [
test "succeeds for multiple fields and directions" {
test "ensureIndexOn succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn
"test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL)
(Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ])
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
"((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ]
"((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)" ]
|> String.concat "")
"CREATE INDEX for multiple field statement incorrect"
}
test "succeeds for nested PostgreSQL field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))"
"CREATE INDEX for nested PostgreSQL field incorrect"
}
test "succeeds for nested SQLite field" {
Expect.equal
(Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite)
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->'a'->'b'->>'c'))"
"CREATE INDEX for nested SQLite field incorrect"
}
]
]
test "insert succeeds" {
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct"
@ -408,167 +135,6 @@ let queryTests = testList "Query" [
$"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
test "count succeeds" {
Expect.equal (Query.count tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct"
}
test "exists succeeds" {
Expect.equal
(Query.exists tbl "turkey")
$"SELECT EXISTS (SELECT 1 FROM {tbl} WHERE turkey) AS it"
"Exists query not correct"
}
test "find succeeds" {
Expect.equal (Query.find tbl) $"SELECT data FROM {tbl}" "Find query not correct"
}
test "update succeeds" {
Expect.equal (Query.update tbl) $"UPDATE {tbl} SET data = @data" "Update query not correct"
}
test "delete succeeds" {
Expect.equal (Query.delete tbl) $"DELETE FROM {tbl}" "Delete query not correct"
}
testList "orderBy" [
test "succeeds for no fields" {
Expect.equal (Query.orderBy [] PostgreSQL) "" "Order By should have been blank (PostgreSQL)"
Expect.equal (Query.orderBy [] SQLite) "" "Order By should have been blank (SQLite)"
}
test "succeeds for PostgreSQL with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] PostgreSQL)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for SQLite with one field and no direction" {
Expect.equal
(Query.orderBy [ Field.Named "TestField" ] SQLite)
" ORDER BY data->>'TestField'"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
PostgreSQL)
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for SQLite with multiple fields and direction" {
Expect.equal
(Query.orderBy
[ Field.Named "Nested.Test.Field DESC"; Field.Named "AnotherField"; Field.Named "It DESC" ]
SQLite)
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC"
"Order By not constructed correctly"
}
test "succeeds for PostgreSQL numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] PostgreSQL)
" ORDER BY (data->>'Test')::numeric"
"Order By not constructed correctly for numeric field"
}
test "succeeds for SQLite numeric fields" {
Expect.equal
(Query.orderBy [ Field.Named "n:Test" ] SQLite)
" ORDER BY data->>'Test'"
"Order By not constructed correctly for numeric field"
}
test "succeeds for PostgreSQL case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field DESC NULLS FIRST" ] PostgreSQL)
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST"
"Order By not constructed correctly for case-insensitive field"
}
test "succeeds for SQLite case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field ASC NULLS LAST" ] SQLite)
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST"
"Order By not constructed correctly for case-insensitive field"
}
]
]
let private streamText (stream: Stream) =
stream.Position <- 0L
use reader = new StreamReader(stream)
reader.ReadToEnd()
/// Unit tests for the PipeWriter module
let pipeWriterTests = testList "Extensions.PipeWriter" [
testList "writeString" [
testTask "succeeds when writer is open" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
try
let! result = PipeWriter.writeString writer "abc"
Expect.isTrue result "The write operation should have been successful"
Expect.equal (streamText stream) "abc" "The string was not written correctly"
finally
writer.Complete()
}
testTask "succeeds when writer is completed" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
do! writer.CompleteAsync()
let! result = PipeWriter.writeString writer "abc"
Expect.isFalse result "The write operation should have returned false"
Expect.equal (streamText stream) "" "No text should have been written"
}
]
testList "writeStrings" [
testTask "succeeds with no strings" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
try
do! PipeWriter.writeStrings writer []
Expect.equal (streamText stream) "[]" "An empty sequence of strings was not written correctly"
finally
writer.Complete()
}
testTask "succeeds with one strings" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
try
do! PipeWriter.writeStrings writer [ "le-test" ]
Expect.equal (streamText stream) "[le-test]" "A sequence of one string was not written correctly"
finally
writer.Complete()
}
testTask "succeeds with many strings" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
try
do! PipeWriter.writeStrings writer [ "z"; "y"; "x"; "c"; "b"; "a" ]
Expect.equal (streamText stream) "[z,y,x,c,b,a]" "A sequence of many strings was not written correctly"
finally
writer.Complete()
}
testTask "succeeds when the writer is completed early" {
use stream = new MemoryStream()
let writer = PipeWriter.Create(stream, StreamPipeWriterOptions(leaveOpen = true))
let items = seq {
"a"
"b"
"c"
writer.Complete()
"d"
"e"
"f"
}
do! PipeWriter.writeStrings writer items
Expect.equal (streamText stream) "[a,b,c" "The writing should have stopped when the writer completed"
}
]
]
/// Tests which do not hit the database
let all = testList "Common" [
comparisonTests
fieldTests
fieldMatchTests
parameterNameTests
autoIdTests
queryTests
pipeWriterTests
testSequenced configurationTests
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +1,23 @@
module Types
type NumIdDocument =
{ Key: int
Text: string }
type SubDocument =
{ Foo: string
Bar: string }
type ArrayDocument =
{ Id: string
Values: string list }
with
/// <summary>
/// A set of documents used for integration tests
/// </summary>
static member TestDocuments =
[ { Id = "first"; Values = [ "a"; "b"; "c" ] }
{ Id = "second"; Values = [ "c"; "d"; "e" ] }
{ Id = "third"; Values = [ "x"; "y"; "z" ] } ]
type JsonDocument =
{ Id: string
Value: string
NumValue: int
Sub: SubDocument option }
module JsonDocument =
/// The JSON for document ID `one`
let one = """{"Id":"one","Value":"FIRST!","NumValue":0,"Sub":null}"""
/// The JSON for document ID `two`
let two = """{"Id":"two","Value":"another","NumValue":10,"Sub":{"Foo":"green","Bar":"blue"}}"""
/// The JSON for document ID `three`
let three = """{"Id":"three","Value":"","NumValue":4,"Sub":null}"""
/// The JSON for document ID `four`
let four = """{"Id":"four","Value":"purple","NumValue":17,"Sub":{"Foo":"green","Bar":"red"}}"""
/// The JSON for document ID `five`
let five = """{"Id":"five","Value":"purple","NumValue":18,"Sub":null}"""
/// An empty JsonDocument
let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None }
/// Documents to use for testing
let testDocuments =
[ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
let testDocuments = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
{ Id = "three"; Value = ""; NumValue = 4; Sub = None }
{ Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } }
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None } ]
{ Id = "five"; Value = "purple"; NumValue = 18; Sub = None }
]

View File

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

View File

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

View File

@ -1,4 +0,0 @@
- name: Docs
href: docs/getting-started.md
- name: API
href: api/