From 2c24e2e912b4618952d33bb75e05e5252a7ee486 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 19 Aug 2024 23:30:38 +0000 Subject: [PATCH] Version 4 rc1 (#6) Changes in this version: - **BREAKING CHANGE**: All `*byField`/`*ByField` functions are now `*byFields`/`*ByFields`, and take a `FieldMatch` case before the list of fields. The `Compat` namespace in both libraries will assist in this transition. In support of this change, the `Field` parameter name is optional; the library will generate parameter names for it if they are not specified. - **BREAKING CHANGE**: The `Query` namespaces have had some significant work, particularly from the full-query perspective. Most have been broken up into the base query and modifiers `by*` that will combine the base query with the `WHERE` clause needed to satisfy the criteria. - **FEATURE / BREAKING CHANGE**: PostgreSQL document fields will now be cast to numeric if the parameter value passed to the query is numeric. This drove the `Query` breaking changes, as the fields need to have their intended value for the library to generate the appropriate SQL. Additionally, if code assumes the library can be given something like `8` and transform it to `"8"`, this is no longer the case. - **FEATURE**: All `Find` queries (except `byId`/`ById`) now have a version with the `Ordered` suffix. These take a list of fields by which the query should be ordered. A new `Field` method called `Named` can assist with creating these fields. Prefixing the field name with `n:` will cast the field to numeric in PostgreSQL (and will be ignored by SQLite); adding " DESC" to the field name will sort it descending (Z-A, high to low) instead of ascending (A-Z, low to high). - **BREAKING CHANGE** (PostgreSQL only): `fieldNameParam`/`Parameters.FieldName` are now plural. The function still only generates one parameter, but the name is now the same between PostgreSQL and SQLite. The goal of this library is to abstract the differences away as much as practical, and this furthers that end. There are functions with these names in the `Compat` namespace. - **FEATURE**: In the F# v3 library, lists of parameters were expected to be F#'s `List` type, and the C# version took either `List` or `IEnumerable`. In this version, these all expect `seq`/`IEnumerable`. F#'s `List` satisfies the `seq` constraints, so this should not be a breaking change. - **FEATURE**: `Field`s now may have qualifiers; this allows tables to be aliased when joining multiple tables (as all have the same `data` column). F# users can use `with` to specify this at creation, and both F# and C# can use the `WithQualifier` method to create a field with the qualifier specified. Parameter names for fields may be specified in a similar way, substituting `ParameterName` for `Qualifier`. Reviewed-on: https://git.bitbadger.solutions/bit-badger/BitBadger.Documents/pulls/6 --- .gitignore | 1 + src/Common/Library.fs | 245 +- src/Directory.Build.props | 9 +- .../BitBadger.Documents.Postgres.fsproj | 1 + src/Postgres/Compat.fs | 270 ++ src/Postgres/Extensions.fs | 153 +- src/Postgres/Library.fs | 811 ++--- src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 1 + src/Sqlite/Compat.fs | 269 ++ src/Sqlite/Extensions.fs | 149 +- src/Sqlite/Library.fs | 567 ++-- .../BitBadger.Documents.Tests.CSharp.csproj | 1 + src/Tests.CSharp/CommonCSharpTests.cs | 781 +++-- .../PostgresCSharpExtensionTests.cs | 532 +++- src/Tests.CSharp/PostgresCSharpTests.cs | 2685 ++++++++++------- src/Tests.CSharp/PostgresDb.cs | 3 +- .../SqliteCSharpExtensionTests.cs | 304 +- src/Tests.CSharp/SqliteCSharpTests.cs | 1417 +++++---- src/Tests.CSharp/Types.cs | 6 + src/Tests/BitBadger.Documents.Tests.fsproj | 3 +- src/Tests/CommonTests.fs | 577 +++- src/Tests/PostgresExtensionTests.fs | 282 +- src/Tests/PostgresTests.fs | 2404 ++++++++------- src/Tests/SqliteExtensionTests.fs | 150 +- src/Tests/SqliteTests.fs | 1477 ++++----- src/Tests/Types.fs | 4 + src/package.sh | 3 + 27 files changed, 8162 insertions(+), 4943 deletions(-) create mode 100644 src/Postgres/Compat.fs create mode 100644 src/Sqlite/Compat.fs diff --git a/.gitignore b/.gitignore index 8a30d25..68e3c10 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +**/.idea diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 77dc4c1..428c859 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -1,5 +1,7 @@ namespace BitBadger.Documents +open System.Security.Cryptography + /// The types of logical operations available for JSON fields [] type Op = @@ -35,6 +37,12 @@ type Op = | NEX -> "IS NULL" +/// The dialect in which a command should be rendered +[] +type Dialect = + | PostgreSQL + | SQLite + /// Criteria for a field WHERE clause type Field = { /// The name of the field @@ -45,43 +53,166 @@ type Field = { /// The value of the field Value: obj + + /// The name of the parameter for this field + ParameterName: string option + + /// The table qualifier for this field + Qualifier: string option } with /// Create an equals (=) field criterion static member EQ name (value: obj) = - { Name = name; Op = EQ; Value = value } + { Name = name; Op = EQ; Value = value; ParameterName = None; Qualifier = None } /// Create a greater than (>) field criterion static member GT name (value: obj) = - { Name = name; Op = GT; Value = value } + { Name = name; Op = GT; Value = value; ParameterName = None; Qualifier = None } /// Create a greater than or equal to (>=) field criterion static member GE name (value: obj) = - { Name = name; Op = GE; Value = value } + { Name = name; Op = GE; Value = value; ParameterName = None; Qualifier = None } /// Create a less than (<) field criterion static member LT name (value: obj) = - { Name = name; Op = LT; Value = value } + { Name = name; Op = LT; Value = value; ParameterName = None; Qualifier = None } /// Create a less than or equal to (<=) field criterion static member LE name (value: obj) = - { Name = name; Op = LE; Value = value } + { Name = name; Op = LE; Value = value; ParameterName = None; Qualifier = None } /// Create a not equals (<>) field criterion static member NE name (value: obj) = - { Name = name; Op = NE; Value = value } + { Name = name; Op = NE; Value = value; ParameterName = None; Qualifier = None } /// Create a BETWEEN field criterion static member BT name (min: obj) (max: obj) = - { Name = name; Op = BT; Value = [ min; max ] } + { Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None } /// Create an exists (IS NOT NULL) field criterion static member EX name = - { Name = name; Op = EX; Value = obj () } + { Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None } /// Create a not exists (IS NULL) field criterion static member NEX name = - { Name = name; Op = NEX; Value = obj () } + { Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None } + + /// Transform a field name (a.b.c) to a path for the given SQL dialect + static member NameToPath (name: string) dialect = + let path = + if name.Contains '.' then + match dialect with + | PostgreSQL -> "#>>'{" + String.concat "," (name.Split '.') + "}'" + | SQLite -> "->>'" + String.concat "'->>'" (name.Split '.') + "'" + else $"->>'{name}'" + $"data{path}" + + /// Create a field with a given name, but no other properties filled (op will be EQ, value will be "") + static member Named name = + { Name = name; Op = EQ; Value = ""; ParameterName = None; Qualifier = None } + + /// Specify the name of the parameter for this field + member this.WithParameterName name = + { this with ParameterName = Some name } + + /// Specify a qualifier (alias) for the table from which this field will be referenced + member this.WithQualifier alias = + { this with Qualifier = Some alias } + + /// Get the qualified path to the field + member this.Path dialect = + (this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect + + +/// How fields should be matched +[] +type FieldMatch = + /// Any field matches (OR) + | Any + /// All fields match (AND) + | All + + /// The SQL value implementing each matching strategy + override this.ToString() = + match this with Any -> "OR" | All -> "AND" + + +/// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) +type ParameterName() = + /// The counter for the next field value + let mutable currentIdx = -1 + + /// Return the specified name for the parameter, or an anonymous parameter name if none is specified + member this.Derive paramName = + match paramName with + | Some it -> it + | None -> + currentIdx <- currentIdx + 1 + $"@field{currentIdx}" + +#if NET6_0 +open System.Text +#endif + +/// Automatically-generated document ID strategies +[] +type AutoId = + /// No automatic IDs will be generated + | Disabled + /// Generate a MAX-plus-1 numeric value for documents + | Number + /// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string) + | Guid + /// Generate a random string of hexadecimal characters for each document + | RandomString +with + /// Generate a GUID string + static member GenerateGuid () = + System.Guid.NewGuid().ToString "N" + + /// Generate a string of random hexadecimal characters + static member GenerateRandomString (length: int) = +#if NET8_0_OR_GREATER + RandomNumberGenerator.GetHexString(length, lowercase = true) +#else + RandomNumberGenerator.GetBytes((length / 2) + 1) + |> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length) + |> function it -> it.Length <- length; it.ToString() +#endif + + /// Does the given document need an automatic ID generated? + 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 then + let value = prop.GetValue document :?> int8 + value = int8 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int16 + value = int16 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int + value = 0 + elif prop.PropertyType = typeof 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 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 /// The required document serialization implementation @@ -135,7 +266,7 @@ module Configuration = serializerValue /// The serialized name of the ID field for documents - let mutable idFieldValue = "Id" + let mutable private idFieldValue = "Id" /// Specify the name of the ID field for documents [] @@ -146,16 +277,42 @@ module Configuration = [] let idField () = idFieldValue + + /// The automatic ID strategy used by the library + let mutable private autoIdValue = Disabled + + /// Specify the automatic ID generation strategy used by the library + [] + let useAutoIdStrategy it = + autoIdValue <- it + + /// Retrieve the currently configured automatic ID generation strategy + [] + let autoIdStrategy () = + autoIdValue + + /// The length of automatically generated random strings + let mutable private idStringLengthValue = 16 + + /// Specify the length of automatically generated random strings + [] + let useIdStringLength length = + idStringLengthValue <- length + + /// Retrieve the currently configured length of automatically generated random strings + [] + let idStringLength () = + idStringLengthValue /// Query construction functions [] module Query = - /// Create a SELECT clause to retrieve the document data from the given table - [] - let selectFromTable tableName = - $"SELECT data FROM %s{tableName}" + /// Combine a query (select, update, etc.) and a WHERE clause + [] + let statementWhere statement where = + $"%s{statement} WHERE %s{where}" /// Queries to define tables and indexes module Definition = @@ -172,7 +329,7 @@ module Query = /// SQL statement to create an index on one or more fields in a JSON document [] - let ensureIndexOn tableName indexName (fields: string seq) = + let ensureIndexOn tableName indexName (fields: string seq) dialect = let _, tbl = splitSchemaAndTable tableName let jsonFields = fields @@ -180,14 +337,14 @@ module Query = let parts = it.Split ' ' let fieldName = if Array.length parts = 1 then it else parts[0] let direction = if Array.length parts < 2 then "" else $" {parts[1]}" - $"(data ->> '{fieldName}'){direction}") + $"({Field.NameToPath fieldName dialect}){direction}") |> String.concat ", " $"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})" /// SQL statement to create a key index for a document table [] - let ensureKey tableName = - (ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX") + let ensureKey tableName dialect = + (ensureIndexOn tableName "key" [ Configuration.idField () ] dialect).Replace("INDEX", "UNIQUE INDEX") /// Query to insert a document [] @@ -200,4 +357,54 @@ module Query = sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) - \ No newline at end of file + + /// Query to count documents in a table (no WHERE clause) + [] + let count tableName = + $"SELECT COUNT(*) AS it FROM %s{tableName}" + + /// Query to check for document existence in a table + [] + let exists tableName where = + $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE %s{where}) AS it" + + /// Query to select documents from a table (no WHERE clause) + [] + let find tableName = + $"SELECT data FROM %s{tableName}" + + /// Query to update a document (no WHERE clause) + [] + let update tableName = + $"UPDATE %s{tableName} SET data = @data" + + /// Query to delete documents from a table (no WHERE clause) + [] + let delete tableName = + $"DELETE FROM %s{tableName}" + + /// Create a SELECT clause to retrieve the document data from the given table + [] + [] + let selectFromTable tableName = + find tableName + + /// Create an ORDER BY clause for the given fields + [] + 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[1]}" + else it, None) + |> Seq.map (fun (field, direction) -> + match dialect, field.Name.StartsWith "n:" with + | PostgreSQL, true -> $"({ { field with Name = field.Name[2..] }.Path PostgreSQL})::numeric" + | SQLite, true -> { field with Name = field.Name[2..] }.Path SQLite + | _, _ -> field.Path dialect + |> function path -> path + defaultArg direction "") + |> String.concat ", " + |> function it -> $" ORDER BY {it}" diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ea70aa3..58be66d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,11 @@ net6.0;net8.0 embedded false - 3.1.0.0 - 3.1.0.0 - 3.1.0 - Add BT (between) operator; drop .NET 7 support + 4.0.0.0 + 4.0.0.0 + 4.0.0 + rc1 + Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility danieljsummers Bit Badger Solutions README.md diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index 1504c4a..6773b53 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Postgres/Compat.fs b/src/Postgres/Compat.fs new file mode 100644 index 0000000..dae2bfa --- /dev/null +++ b/src/Postgres/Compat.fs @@ -0,0 +1,270 @@ +namespace BitBadger.Documents.Postgres.Compat + +open BitBadger.Documents +open BitBadger.Documents.Postgres + +[] +module Parameters = + + /// Create a JSON field parameter + [] + [] + 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 + [] + [] + let fieldNameParam fieldNames = + fieldNameParams fieldNames + + +[] +module Query = + + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document + [] + [] + let whereByField field paramName = + Query.whereByFields Any [ { field with ParameterName = Some paramName } ] + + +module WithProps = + + [] + module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Count.byFields tableName Any [ field ] sqlProps + + [] + module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Exists.byFields tableName Any [ field ] sqlProps + + [] + module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field sqlProps = + WithProps.Find.byFields<'TDoc> tableName Any [ field ] sqlProps + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + 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 + [] + [] + 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 + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = + WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) + + [] + module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) sqlProps = + WithProps.Patch.byFields tableName Any [ field ] patch sqlProps + + [] + module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames sqlProps = + WithProps.RemoveFields.byFields tableName Any [ field ] fieldNames sqlProps + + [] + module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field sqlProps = + WithProps.Delete.byFields tableName Any [ field ] sqlProps + + +[] +module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Count.byFields tableName Any [ field ] + + +[] +module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Exists.byFields tableName Any [ field ] + + +[] +module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field = + Find.byFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + 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 + [] + [] + 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 + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) + + +[] +module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) = + Patch.byFields tableName Any [ field ] patch + + +[] +module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames = + RemoveFields.byFields tableName Any [ field ] fieldNames + + +[] +module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field = + Delete.byFields tableName Any [ field ] + + +open Npgsql + +/// F# Extensions for the NpgsqlConnection type +[] +module Extensions = + + type NpgsqlConnection with + + /// Count matching documents using a JSON field comparison query (->> =) + [] + member conn.countByField tableName field = + conn.countByFields tableName Any [ field ] + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + member conn.existsByField tableName field = + conn.existsByFields tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + 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 + [] + member conn.findFirstByField<'TDoc> tableName field = + conn.findFirstByFields<'TDoc> tableName Any [ field ] + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + 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 + [] + member conn.removeFieldsByField tableName field fieldNames = + conn.removeFieldsByFields tableName Any [ field ] fieldNames + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + 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 (->> =) + [] + [] + 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 (->> =) + [] + [] + static member inline ExistsByField(conn, tableName, field) = + WithProps.Exists.byFields tableName Any [ field ] (Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] + 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 + [] + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(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 (->> =) + [] + [] + 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 + [] + [] + 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 (->> =) + [] + [] + static member inline DeleteByField(conn, tableName, field) = + WithProps.Delete.byFields tableName Any [ field ] (Sql.existingConnection conn) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index c2ae2e5..2b3b63a 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -50,8 +50,8 @@ module Extensions = WithProps.Count.all tableName (Sql.existingConnection conn) /// Count matching documents using a JSON field comparison query (->> =) - member conn.countByField tableName field = - WithProps.Count.byField tableName field (Sql.existingConnection conn) + member conn.countByFields tableName howMatched fields = + WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) /// Count matching documents using a JSON containment query (@>) member conn.countByContains tableName criteria = @@ -66,8 +66,8 @@ module Extensions = WithProps.Exists.byId tableName docId (Sql.existingConnection conn) /// Determine if documents exist using a JSON field comparison query (->> =) - member conn.existsByField tableName field = - WithProps.Exists.byField tableName field (Sql.existingConnection conn) + member conn.existsByFields tableName howMatched fields = + WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) /// Determine if documents exist using a JSON containment query (@>) member conn.existsByContains tableName criteria = @@ -81,34 +81,68 @@ module Extensions = member conn.findAll<'TDoc> tableName = WithProps.Find.all<'TDoc> tableName (Sql.existingConnection conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + member conn.findAllOrdered<'TDoc> tableName orderFields = + WithProps.Find.allOrdered<'TDoc> tableName orderFields (Sql.existingConnection conn) + /// Retrieve a document by its ID; returns None if not found member conn.findById<'TKey, 'TDoc> tableName docId = WithProps.Find.byId<'TKey, 'TDoc> tableName docId (Sql.existingConnection conn) /// Retrieve documents matching a JSON field comparison query (->> =) - member conn.findByField<'TDoc> tableName field = - WithProps.Find.byField<'TDoc> tableName field (Sql.existingConnection conn) + member conn.findByFields<'TDoc> tableName howMatched fields = + WithProps.Find.byFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document + member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.byFieldsOrdered<'TDoc> + tableName howMatched queryFields orderFields (Sql.existingConnection conn) /// Retrieve documents matching a JSON containment query (@>) member conn.findByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.byContains<'TDoc> tableName criteria (Sql.existingConnection conn) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + member conn.findByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) member conn.findByJsonPath<'TDoc> tableName jsonPath = WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + member conn.findByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found - member conn.findFirstByField<'TDoc> tableName field = - WithProps.Find.firstByField<'TDoc> tableName field (Sql.existingConnection conn) + member conn.findFirstByFields<'TDoc> tableName howMatched fields = + WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in + /// the document; returns None if not found + member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.firstByFieldsOrdered<'TDoc> + tableName howMatched queryFields orderFields (Sql.existingConnection conn) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found member conn.findFirstByContains<'TDoc> tableName (criteria: obj) = WithProps.Find.firstByContains<'TDoc> tableName criteria (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns None if not found + member conn.findFirstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found member conn.findFirstByJsonPath<'TDoc> tableName jsonPath = WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns None if not found + member conn.findFirstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (Sql.existingConnection conn) + /// Update an entire document by its ID member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = WithProps.Update.byId tableName docId document (Sql.existingConnection conn) @@ -122,8 +156,8 @@ module Extensions = WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - member conn.patchByField tableName field (patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) + member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = + WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) /// Patch documents using a JSON containment query in the WHERE clause (@>) member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = @@ -137,9 +171,9 @@ module Extensions = member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn) - /// Remove fields from documents via a comparison on a JSON field in the document - member conn.removeFieldsByField tableName field fieldNames = - WithProps.RemoveFields.byField tableName field fieldNames (Sql.existingConnection conn) + /// Remove fields from documents via a comparison on JSON fields in the document + member conn.removeFieldsByFields tableName howMatched fields fieldNames = + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) /// Remove fields from documents via a JSON containment query (@>) member conn.removeFieldsByContains tableName (criteria: 'TContains) fieldNames = @@ -153,9 +187,8 @@ module Extensions = member conn.deleteById tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (Sql.existingConnection conn) - /// Delete documents by matching a JSON field comparison query (->> =) - member conn.deleteByField tableName field = - WithProps.Delete.byField tableName field (Sql.existingConnection conn) + member conn.deleteByFields tableName howMatched fields = + WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) /// Delete documents by matching a JSON containment query (@>) member conn.deleteByContains tableName (criteria: 'TContains) = @@ -225,8 +258,8 @@ type NpgsqlConnectionCSharpExtensions = /// Count matching documents using a JSON field comparison query (->> =) [] - static member inline CountByField(conn, tableName, field) = - WithProps.Count.byField tableName field (Sql.existingConnection conn) + static member inline CountByFields(conn, tableName, howMatched, fields) = + WithProps.Count.byFields tableName howMatched fields (Sql.existingConnection conn) /// Count matching documents using a JSON containment query (@>) [] @@ -245,8 +278,8 @@ type NpgsqlConnectionCSharpExtensions = /// Determine if documents exist using a JSON field comparison query (->> =) [] - static member inline ExistsByField(conn, tableName, field) = - WithProps.Exists.byField tableName field (Sql.existingConnection conn) + static member inline ExistsByFields(conn, tableName, howMatched, fields) = + WithProps.Exists.byFields tableName howMatched fields (Sql.existingConnection conn) /// Determine if documents exist using a JSON containment query (@>) [] @@ -263,6 +296,11 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindAll<'TDoc>(conn, tableName) = WithProps.Find.All<'TDoc>(tableName, Sql.existingConnection conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) = + WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, Sql.existingConnection conn) + /// Retrieve a document by its ID; returns None if not found [] static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = @@ -270,34 +308,71 @@ type NpgsqlConnectionCSharpExtensions = /// Retrieve documents matching a JSON field comparison query (->> =) [] - static member inline FindByField<'TDoc>(conn, tableName, field) = - WithProps.Find.ByField<'TDoc>(tableName, field, Sql.existingConnection conn) + static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = + WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + [] + static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) = + WithProps.Find.ByFieldsOrdered<'TDoc>( + tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) /// Retrieve documents matching a JSON containment query (@>) [] static member inline FindByContains<'TDoc>(conn, tableName, criteria: obj) = WithProps.Find.ByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + static member inline FindByContainsOrdered<'TDoc>(conn, tableName, criteria: obj, orderFields) = + WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) + /// Retrieve documents matching a JSON Path match query (@?) [] static member inline FindByJsonPath<'TDoc>(conn, tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) - /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - WithProps.Find.FirstByField<'TDoc>(tableName, field, Sql.existingConnection conn) + static member inline FindByJsonPathOrdered<'TDoc>(conn, tableName, jsonPath, orderFields) = + WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + [] + static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) + + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns null if not found + [] + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, howMatched, queryFields, orderFields) = + WithProps.Find.FirstByFieldsOrdered<'TDoc>( + tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns None if not found + [] + static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, criteria: obj, orderFields) = + WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns None if not found + [] + static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null>(conn, tableName, jsonPath, orderFields) = + WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) + /// Update an entire document by its ID [] static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) = @@ -315,8 +390,8 @@ type NpgsqlConnectionCSharpExtensions = /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] - static member inline PatchByField(conn, tableName, field, patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (Sql.existingConnection conn) + static member inline PatchByFields(conn, tableName, howMatched, fields, patch: 'TPatch) = + WithProps.Patch.byFields tableName howMatched fields patch (Sql.existingConnection conn) /// Patch documents using a JSON containment query in the WHERE clause (@>) [] @@ -331,32 +406,32 @@ type NpgsqlConnectionCSharpExtensions = /// Remove fields from a document by the document's ID [] static member inline RemoveFieldsById(conn, tableName, docId: 'TKey, fieldNames) = - WithProps.RemoveFields.ById(tableName, docId, fieldNames, Sql.existingConnection conn) - - /// Remove fields from documents via a comparison on a JSON field in the document + WithProps.RemoveFields.byId tableName docId fieldNames (Sql.existingConnection conn) + + /// Remove fields from documents via a comparison on JSON fields in the document [] - static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - WithProps.RemoveFields.ByField(tableName, field, fieldNames, Sql.existingConnection conn) - + static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (Sql.existingConnection conn) + /// Remove fields from documents via a JSON containment query (@>) [] static member inline RemoveFieldsByContains(conn, tableName, criteria: 'TContains, fieldNames) = - WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, Sql.existingConnection conn) - + WithProps.RemoveFields.byContains tableName criteria fieldNames (Sql.existingConnection conn) + /// Remove fields from documents via a JSON Path match query (@?) [] static member inline RemoveFieldsByJsonPath(conn, tableName, jsonPath, fieldNames) = - WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, Sql.existingConnection conn) + WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (Sql.existingConnection conn) /// Delete a document by its ID [] static member inline DeleteById(conn, tableName, docId: 'TKey) = WithProps.Delete.byId tableName docId (Sql.existingConnection conn) - + /// Delete documents by matching a JSON field comparison query (->> =) [] - static member inline DeleteByField(conn, tableName, field) = - WithProps.Delete.byField tableName field (Sql.existingConnection conn) + static member inline DeleteByFields(conn, tableName, howMatched, fields) = + WithProps.Delete.byFields tableName howMatched fields (Sql.existingConnection conn) /// Delete documents by matching a JSON containment query (@>) [] diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index cd551d4..4095549 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -45,6 +45,23 @@ module private Helpers = let! _ = it () } + + /// Create a number or string parameter, or use the given parameter derivation function if non-(numeric or string) + let internal parameterFor<'T> (value: 'T) (catchAllFunc: 'T -> SqlValue) = + match box value with + | :? int8 as it -> Sql.int8 it + | :? uint8 as it -> Sql.int8 (int8 it) + | :? int16 as it -> Sql.int16 it + | :? uint16 as it -> Sql.int16 (int16 it) + | :? int as it -> Sql.int it + | :? uint32 as it -> Sql.int (int it) + | :? int64 as it -> Sql.int64 it + | :? uint64 as it -> Sql.int64 (int64 it) + | :? decimal as it -> Sql.decimal it + | :? single as it -> Sql.double (double it) + | :? double as it -> Sql.double it + | :? string as it -> Sql.string it + | _ -> catchAllFunc value open BitBadger.Documents @@ -53,50 +70,44 @@ open BitBadger.Documents [] module Parameters = - /// Create an ID parameter (name "@id", key will be treated as a string) + /// Create an ID parameter (name "@id") [] let idParam (key: 'TKey) = - "@id", Sql.string (string key) + "@id", parameterFor key (fun it -> Sql.string (string it)) /// Create a parameter with a JSON value [] let jsonParam (name: string) (it: 'TJson) = name, Sql.jsonb (Configuration.serializer().Serialize it) - - /// Create a JSON field parameter - [] - let addFieldParam name field parameters = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - List.concat - [ parameters - [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values))) - ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] ] - | _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) :: parameters - - /// Create a JSON field parameter - let AddField name field parameters = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - ResizeArray - [ ($"{name}min", Sql.parameter (NpgsqlParameter($"{name}min", List.head values))) - ($"{name}max", Sql.parameter (NpgsqlParameter($"{name}max", List.last values))) ] - |> Seq.append parameters - | _ -> (name, Sql.parameter (NpgsqlParameter(name, field.Value))) |> Seq.singleton |> Seq.append parameters + + /// Create JSON field parameters + [] + let addFieldParams fields parameters = + let name = ParameterName() + fields + |> Seq.map (fun it -> + seq { + match it.Op with + | EX | NEX -> () + | BT -> + let p = name.Derive it.ParameterName + let values = it.Value :?> obj list + yield ($"{p}min", + parameterFor (List.head values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}min", v)))) + yield ($"{p}max", + parameterFor (List.last values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}max", v)))) + | _ -> + let p = name.Derive it.ParameterName + yield (p, parameterFor it.Value (fun v -> Sql.parameter (NpgsqlParameter(p, v)))) }) + |> Seq.collect id + |> Seq.append parameters + |> Seq.toList + |> Seq.ofList /// Append JSON field name parameters for the given field names to the given parameters - [] - let fieldNameParam (fieldNames: string list) = - if fieldNames.Length = 1 then "@name", Sql.string fieldNames[0] - else "@name", Sql.stringArray (Array.ofList fieldNames) - - /// Append JSON field name parameters for the given field names to the given parameters - let FieldName(fieldNames: string seq) = - if Seq.isEmpty fieldNames then "@name", Sql.string (Seq.head fieldNames) + [] + let fieldNameParams (fieldNames: string seq) = + if Seq.length fieldNames = 1 then "@name", Sql.string (Seq.head fieldNames) else "@name", Sql.stringArray (Array.ofSeq fieldNames) /// An empty parameter sequence @@ -109,24 +120,34 @@ module Parameters = [] module Query = - /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document - [] - let whereByField field paramName = - match field.Op with - | EX | NEX -> $"data->>'{field.Name}' {field.Op}" - | BT -> - let names = $"{paramName}min AND {paramName}max" - let values = field.Value :?> obj list - match values[0] with + /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document + [] + let whereByFields (howMatched: FieldMatch) fields = + let name = ParameterName() + let isNumeric (it: obj) = + match it with | :? int8 | :? uint8 | :? int16 | :? uint16 | :? int | :? uint32 | :? int64 | :? uint64 - | :? decimal | :? single | :? double -> $"(data->>'{field.Name}')::numeric {field.Op} {names}" - | _ -> $"data->>'{field.Name}' {field.Op} {names}" - | _ -> $"data->>'{field.Name}' {field.Op} %s{paramName}" - + | :? decimal | :? single | :? double -> true + | _ -> false + fields + |> Seq.map (fun it -> + match it.Op with + | EX | NEX -> $"{it.Path PostgreSQL} {it.Op}" + | _ -> + let p = name.Derive it.ParameterName + let param, value = + match it.Op with + | BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0] + | _ -> p, it.Value + if isNumeric value then + $"({it.Path PostgreSQL})::numeric {it.Op} {param}" + else $"{it.Path PostgreSQL} {it.Op} {param}") + |> String.concat $" {howMatched} " + /// Create a WHERE clause fragment to implement an ID-based query [] - let whereById paramName = - whereByField (Field.EQ (Configuration.idField ()) 0) paramName + let whereById<'TKey> (docId: 'TKey) = + whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ] /// Table and index definition queries module Definition = @@ -143,11 +164,6 @@ module Query = let tableName = name.Split '.' |> Array.last $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" - /// Query to update a document - [] - let update tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Create a WHERE clause fragment to implement a @> (JSON contains) condition [] let whereDataContains paramName = @@ -158,151 +174,35 @@ module Query = let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" - /// Queries for counting documents - module Count = - - /// Query to count all documents in a table - [] - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - - /// Query to count matching documents using a text comparison on a JSON field - [] - let byField tableName field = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField field "@field"}""" - - /// Query to count matching documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to count matching documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" + /// Create an UPDATE statement to patch documents + [] + let patch tableName = + $"UPDATE %s{tableName} SET data = data || @data" + + /// Create an UPDATE statement to remove fields from documents + [] + let removeFields tableName = + $"UPDATE %s{tableName} SET data = data - @name" + + /// Create a query by a document's ID + [] + let byId<'TKey> statement (docId: 'TKey) = + Query.statementWhere statement (whereById docId) - /// Queries for determining document existence - module Exists = - - /// Query to determine if a document exists for the given ID - [] - 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 - [] - let byField tableName field = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField field "@field"}) AS it""" - - /// Query to determine if documents exist using a JSON containment query (@>) - [] - let byContains tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it""" - - /// Query to determine if documents exist using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}) AS it""" + /// Create a query on JSON fields + [] + let byFields statement howMatched fields = + Query.statementWhere statement (whereByFields howMatched fields) - /// Queries for retrieving documents - module Find = - - /// Query to retrieve a document by its ID - [] - let byId tableName = - $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" - - /// Query to retrieve documents using a comparison on a JSON field - [] - let byField tableName field = - $"""{Query.selectFromTable tableName} WHERE {whereByField field "@field"}""" - - /// Query to retrieve documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to retrieve documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" + /// Create a JSON containment query + [] + let byContains statement = + Query.statementWhere statement (whereDataContains "@criteria") - /// Queries to patch (partially update) documents - module Patch = - - /// Create an UPDATE statement to patch documents - let private update tableName whereClause = - $"UPDATE %s{tableName} SET data = data || @data WHERE {whereClause}" - - /// Query to patch a document by its ID - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to patch documents match a JSON field comparison (->> =) - [] - let byField tableName field = - whereByField field "@field" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byContains tableName = - whereDataContains "@criteria" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byJsonPath tableName = - whereJsonPathMatches "@path" |> update tableName - - /// Queries to remove fields from documents - module RemoveFields = - - /// Create an UPDATE statement to remove parameters - let private update tableName whereClause = - $"UPDATE %s{tableName} SET data = data - @name WHERE {whereClause}" - - /// Query to remove fields from a document by the document's ID - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to remove fields from documents via a comparison on a JSON field within the document - [] - let byField tableName field = - whereByField field "@field" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byContains tableName = - whereDataContains "@criteria" |> update tableName - - /// Query to patch documents matching a JSON containment query (@>) - [] - let byJsonPath tableName = - whereJsonPathMatches "@path" |> update tableName - - /// Queries to delete documents - module Delete = - - /// Query to delete a document by its ID - [] - let byId tableName = - $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - - /// Query to delete documents using a comparison on a JSON field - [] - let byField tableName field = - $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" - - /// Query to delete documents using a JSON containment query (@>) - [] - let byContains tableName = - $"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" - - /// Query to delete documents using a JSON Path match (@?) - [] - let byJsonPath tableName = - $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" + /// Create a JSON Path match query + [] + let byPathMatch statement = + Query.statementWhere statement (whereJsonPathMatches "@path") /// Functions for dealing with results @@ -333,6 +233,8 @@ module Results = /// Versions of queries that accept SqlProps as the last parameter module WithProps = + module FSharpList = Microsoft.FSharp.Collections.List + /// Commands to execute custom SQL queries [] module Custom = @@ -341,12 +243,12 @@ module WithProps = [] let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps = Sql.query query sqlProps - |> Sql.parameters parameters + |> Sql.parameters (List.ofSeq parameters) |> Sql.executeAsync mapFunc /// Execute a query that returns a list of results let List<'TDoc>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps + let! results = list<'TDoc> query parameters mapFunc.Invoke sqlProps return ResizeArray results } @@ -360,7 +262,7 @@ module WithProps = /// Execute a query that returns one or no results; returns null if not found let Single<'TDoc when 'TDoc: null>( query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! result = single<'TDoc> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps return Option.toObj result } @@ -368,7 +270,7 @@ module WithProps = [] let nonQuery query parameters sqlProps = Sql.query query sqlProps - |> Sql.parameters (FSharp.Collections.List.ofSeq parameters) + |> Sql.parameters (FSharpList.ofSeq parameters) |> Sql.executeNonQueryAsync |> ignoreTask @@ -376,12 +278,12 @@ module WithProps = [] let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = Sql.query query sqlProps - |> Sql.parameters parameters + |> Sql.parameters (FSharpList.ofSeq parameters) |> Sql.executeRowAsync mapFunc /// Execute a query that returns a scalar value let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func, sqlProps) = - scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + scalar<'T> query parameters mapFunc.Invoke sqlProps /// Table and index definition commands module Definition = @@ -390,7 +292,7 @@ module WithProps = [] let ensureTable name sqlProps = backgroundTask { do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps - do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps + do! Custom.nonQuery (Query.Definition.ensureKey name PostgreSQL) [] sqlProps } /// Create an index on documents in the specified table @@ -401,7 +303,7 @@ module WithProps = /// Create an index on field(s) within documents in the specified table [] let ensureFieldIndex tableName indexName fields sqlProps = - Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] sqlProps + Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields PostgreSQL) [] sqlProps /// Commands to add documents [] @@ -410,7 +312,23 @@ module WithProps = /// Insert a new document [] let insert<'TDoc> tableName (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps + 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}')::numeric), 0) + 1 FROM {tableName}) || '" + | Guid -> $"\"{AutoId.GenerateGuid()}\"" + | RandomString -> $"\"{AutoId.GenerateRandomString(Configuration.idStringLength ())}\"" + | Disabled -> "@data" + |> function it -> $"""@data::jsonb || ('{{"{idField}":{it}}}')::jsonb""" + else "@data" + (Query.insert tableName).Replace("@data", dataParam) + Custom.nonQuery query [ jsonParam "@data" document ] sqlProps /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] @@ -424,22 +342,25 @@ module WithProps = /// Count all documents in a table [] let all tableName sqlProps = - Custom.scalar (Query.Count.all tableName) [] toCount sqlProps + Custom.scalar (Query.count tableName) [] toCount sqlProps - /// Count matching documents using a JSON field comparison (->> =) - [] - let byField tableName field sqlProps = - Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount sqlProps + /// Count matching documents using JSON field comparisons (->> =) + [] + let byFields tableName howMatched fields sqlProps = + Custom.scalar + (Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount sqlProps /// Count matching documents using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = - Custom.scalar (Query.Count.byContains tableName) [ jsonParam "@criteria" criteria ] toCount sqlProps + Custom.scalar + (Query.byContains (Query.count tableName)) [ jsonParam "@criteria" criteria ] toCount sqlProps /// Count matching documents using a JSON Path match query (@?) [] let byJsonPath tableName jsonPath sqlProps = - Custom.scalar (Query.Count.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toCount sqlProps + Custom.scalar + (Query.byPathMatch (Query.count tableName)) [ "@path", Sql.string jsonPath ] toCount sqlProps /// Commands to determine if documents exist [] @@ -448,22 +369,34 @@ module WithProps = /// Determine if a document exists for the given ID [] let byId tableName (docId: 'TKey) sqlProps = - Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps + Custom.scalar (Query.exists tableName (Query.whereById docId)) [ idParam docId ] toExists sqlProps - /// Determine if a document exists using a JSON field comparison (->> =) - [] - let byField tableName field sqlProps = - Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists sqlProps - + /// Determine if a document exists using JSON field comparisons (->> =) + [] + let byFields tableName howMatched fields sqlProps = + Custom.scalar + (Query.exists tableName (Query.whereByFields howMatched fields)) + (addFieldParams fields []) + toExists + sqlProps + /// Determine if a document exists using a JSON containment query (@>) [] let byContains tableName (criteria: 'TContains) sqlProps = - Custom.scalar (Query.Exists.byContains tableName) [ jsonParam "@criteria" criteria ] toExists sqlProps + Custom.scalar + (Query.exists tableName (Query.whereDataContains "@criteria")) + [ jsonParam "@criteria" criteria ] + toExists + sqlProps /// Determine if a document exists using a JSON Path match query (@?) [] let byJsonPath tableName jsonPath sqlProps = - Custom.scalar (Query.Exists.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toExists sqlProps + Custom.scalar + (Query.exists tableName (Query.whereJsonPathMatches "@path")) + [ "@path", Sql.string jsonPath ] + toExists + sqlProps /// Commands to determine if documents exist [] @@ -472,81 +405,196 @@ module WithProps = /// Retrieve all documents in the given table [] let all<'TDoc> tableName sqlProps = - Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps + Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> sqlProps /// Retrieve all documents in the given table let All<'TDoc>(tableName, sqlProps) = - Custom.List<'TDoc>(Query.selectFromTable tableName, [], fromData<'TDoc>, sqlProps) + Custom.List<'TDoc>(Query.find tableName, [], fromData<'TDoc>, sqlProps) + + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields sqlProps = + Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields PostgreSQL) [] fromData<'TDoc> sqlProps + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc>(tableName, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.find tableName + Query.orderBy orderFields PostgreSQL, [], fromData<'TDoc>, sqlProps) /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps = - Custom.single (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> sqlProps + Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps /// Retrieve a document by its ID (returns null if not found) let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = - Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, sqlProps) + Custom.Single<'TDoc>( + Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps) - /// Retrieve documents matching a JSON field comparison (->> =) - [] - let byField<'TDoc> tableName field sqlProps = + /// Retrieve documents matching JSON field comparisons (->> =) + [] + let byFields<'TDoc> tableName howMatched fields sqlProps = Custom.list<'TDoc> - (Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> sqlProps + (Query.byFields (Query.find tableName) howMatched fields) + (addFieldParams fields []) + fromData<'TDoc> + sqlProps - /// Retrieve documents matching a JSON field comparison (->> =) - let ByField<'TDoc>(tableName, field, sqlProps) = + /// Retrieve documents matching JSON field comparisons (->> =) + let ByFields<'TDoc>(tableName, howMatched, fields, sqlProps) = Custom.List<'TDoc>( - Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, sqlProps) + Query.byFields (Query.find tableName) howMatched fields, + addFieldParams fields [], + fromData<'TDoc>, + sqlProps) + + /// Retrieve documents matching JSON field comparisons (->> =) ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps = + Custom.list<'TDoc> + (Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL) + (addFieldParams queryFields []) + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching JSON field comparisons (->> =) ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byFields (Query.find tableName) howMatched queryFields + Query.orderBy orderFields PostgreSQL, + addFieldParams queryFields [], + fromData<'TDoc>, + sqlProps) /// Retrieve documents matching a JSON containment query (@>) [] let byContains<'TDoc> tableName (criteria: obj) sqlProps = Custom.list<'TDoc> - (Query.Find.byContains tableName) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps + (Query.byContains (Query.find tableName)) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps /// Retrieve documents matching a JSON containment query (@>) let ByContains<'TDoc>(tableName, criteria: obj, sqlProps) = Custom.List<'TDoc>( - Query.Find.byContains tableName, [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps) + Query.byContains (Query.find tableName), + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps = + Custom.list<'TDoc> + (Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL) + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byContains (Query.find tableName) + Query.orderBy orderFields PostgreSQL, + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) /// Retrieve documents matching a JSON Path match query (@?) [] let byJsonPath<'TDoc> tableName jsonPath sqlProps = Custom.list<'TDoc> - (Query.Find.byJsonPath tableName) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + (Query.byPathMatch (Query.find tableName)) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps /// Retrieve documents matching a JSON Path match query (@?) let ByJsonPath<'TDoc>(tableName, jsonPath, sqlProps) = Custom.List<'TDoc>( - Query.Find.byJsonPath tableName, [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) - - /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found - [] - let firstByField<'TDoc> tableName field sqlProps = - Custom.single<'TDoc> - $"{Query.Find.byField tableName field} LIMIT 1" - (addFieldParam "@field" field []) - fromData<'TDoc> - sqlProps - - /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = - Custom.Single<'TDoc>( - $"{Query.Find.byField tableName field} LIMIT 1", - addFieldParam "@field" field [], + Query.byPathMatch (Query.find tableName), + [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) - + + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + [] + let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps = + Custom.list<'TDoc> + (Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL) + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps + + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, sqlProps) = + Custom.List<'TDoc>( + Query.byPathMatch (Query.find tableName) + Query.orderBy orderFields PostgreSQL, + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching JSON field comparisons (->> =); returns None if not found + [] + let firstByFields<'TDoc> tableName howMatched fields sqlProps = + Custom.single<'TDoc> + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1" + (addFieldParams fields []) + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", + addFieldParams fields [], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + (addFieldParams queryFields []) + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the + /// document; returns null if not found + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1", + addFieldParams queryFields [], + fromData<'TDoc>, + sqlProps) + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = Custom.single<'TDoc> - $"{Query.Find.byContains tableName} LIMIT 1" [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps + $"{Query.byContains (Query.find tableName)} LIMIT 1" + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps /// Retrieve the first document matching a JSON containment query (@>); returns null if not found let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byContains tableName} LIMIT 1", + $"{Query.byContains (Query.find tableName)} LIMIT 1", + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + [ jsonParam "@criteria" criteria ] + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the + /// document; returns null if not found + let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps) @@ -555,12 +603,34 @@ module WithProps = [] let firstByJsonPath<'TDoc> tableName jsonPath sqlProps = Custom.single<'TDoc> - $"{Query.Find.byJsonPath tableName} LIMIT 1" [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + $"{Query.byPathMatch (Query.find tableName)} LIMIT 1" + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) = Custom.Single<'TDoc>( - $"{Query.Find.byJsonPath tableName} LIMIT 1", + $"{Query.byPathMatch (Query.find tableName)} LIMIT 1", + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields sqlProps = + Custom.single<'TDoc> + $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1" + [ "@path", Sql.string jsonPath ] + fromData<'TDoc> + sqlProps + + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the + /// document; returns null if not found + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) @@ -572,7 +642,8 @@ module WithProps = /// Update an entire document by its ID [] let byId tableName (docId: 'TKey) (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] sqlProps + Custom.nonQuery + (Query.byId (Query.update tableName) docId) [ idParam docId; jsonParam "@data" document ] sqlProps /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] @@ -590,77 +661,67 @@ module WithProps = /// Patch a document by its ID [] let byId tableName (docId: 'TKey) (patch: 'TPatch) sqlProps = - Custom.nonQuery (Query.Patch.byId tableName) [ idParam docId; jsonParam "@data" patch ] sqlProps + Custom.nonQuery + (Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] sqlProps /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - let byField tableName field (patch: 'TPatch) sqlProps = + [] + let byFields tableName howMatched fields (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byField tableName field) - (addFieldParam "@field" field [ jsonParam "@data" patch ]) + (Query.byFields (Query.patch tableName) howMatched fields) + (addFieldParams fields [ jsonParam "@data" patch ]) sqlProps /// Patch documents using a JSON containment query in the WHERE clause (@>) [] let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byContains tableName) [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] sqlProps + (Query.byContains (Query.patch tableName)) + [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] + sqlProps /// Patch documents using a JSON Path match query in the WHERE clause (@?) [] let byJsonPath tableName jsonPath (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Patch.byJsonPath tableName) [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] sqlProps + (Query.byPathMatch (Query.patch tableName)) + [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] + sqlProps /// Commands to remove fields from documents [] module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames sqlProps = - Custom.nonQuery (Query.RemoveFields.byId tableName) [ idParam docId; fieldNameParam fieldNames ] sqlProps - - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames, sqlProps) = - byId tableName docId (List.ofSeq fieldNames) sqlProps - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let byField tableName field fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byField tableName field) - (addFieldParam "@field" field [ fieldNameParam fieldNames ]) + (Query.byId (Query.removeFields tableName) docId) [ idParam docId; fieldNameParams fieldNames ] sqlProps + + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames sqlProps = + Custom.nonQuery + (Query.byFields (Query.removeFields tableName) howMatched fields) + (addFieldParams fields [ fieldNameParams fieldNames ]) sqlProps - /// Remove fields from documents via a comparison on a JSON field in the document - let ByField(tableName, field, fieldNames, sqlProps) = - byField tableName field (List.ofSeq fieldNames) sqlProps - /// Remove fields from documents via a JSON containment query (@>) - [] + [] let byContains tableName (criteria: 'TContains) fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byContains tableName) - [ jsonParam "@criteria" criteria; fieldNameParam fieldNames ] + (Query.byContains (Query.removeFields tableName)) + [ jsonParam "@criteria" criteria; fieldNameParams fieldNames ] sqlProps - - /// Remove fields from documents via a JSON containment query (@>) - let ByContains(tableName, criteria: 'TContains, fieldNames, sqlProps) = - byContains tableName criteria (List.ofSeq fieldNames) sqlProps - + /// Remove fields from documents via a JSON Path match query (@?) [] let byJsonPath tableName jsonPath fieldNames sqlProps = Custom.nonQuery - (Query.RemoveFields.byJsonPath tableName) - [ "@path", Sql.string jsonPath; fieldNameParam fieldNames ] + (Query.byPathMatch (Query.removeFields tableName)) + [ "@path", Sql.string jsonPath; fieldNameParams fieldNames ] sqlProps - /// Remove fields from documents via a JSON Path match query (@?) - let ByJsonPath(tableName, jsonPath, fieldNames, sqlProps) = - byJsonPath tableName jsonPath (List.ofSeq fieldNames) sqlProps - /// Commands to delete documents [] module Delete = @@ -668,24 +729,25 @@ module WithProps = /// Delete a document by its ID [] let byId tableName (docId: 'TKey) sqlProps = - Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps + Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] sqlProps /// Delete documents by matching a JSON field comparison query (->> =) - [] - let byField tableName field sqlProps = - Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) sqlProps + [] + let byFields tableName howMatched fields sqlProps = + Custom.nonQuery + (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) sqlProps /// Delete documents by matching a JSON contains query (@>) [] let byContains tableName (criteria: 'TCriteria) sqlProps = - Custom.nonQuery (Query.Delete.byContains tableName) [ jsonParam "@criteria" criteria ] sqlProps + Custom.nonQuery (Query.byContains (Query.delete tableName)) [ jsonParam "@criteria" criteria ] sqlProps /// Delete documents by matching a JSON Path match query (@?) [] let byJsonPath tableName path sqlProps = - Custom.nonQuery (Query.Delete.byJsonPath tableName) [ "@path", Sql.string path ] sqlProps - - + Custom.nonQuery (Query.byPathMatch (Query.delete tableName)) [ "@path", Sql.string path ] sqlProps + + /// Commands to execute custom SQL queries [] module Custom = @@ -768,9 +830,9 @@ module Count = WithProps.Count.all tableName (fromDataSource ()) /// Count matching documents using a JSON field comparison query (->> =) - [] - let byField tableName field = - WithProps.Count.byField tableName field (fromDataSource ()) + [] + let byFields tableName howMatched fields = + WithProps.Count.byFields tableName howMatched fields (fromDataSource ()) /// Count matching documents using a JSON containment query (@>) [] @@ -793,9 +855,9 @@ module Exists = WithProps.Exists.byId tableName docId (fromDataSource ()) /// Determine if documents exist using a JSON field comparison query (->> =) - [] - let byField tableName field = - WithProps.Exists.byField tableName field (fromDataSource ()) + [] + let byFields tableName howMatched fields = + WithProps.Exists.byFields tableName howMatched fields (fromDataSource ()) /// Determine if documents exist using a JSON containment query (@>) [] @@ -821,6 +883,15 @@ module Find = let All<'TDoc> tableName = WithProps.Find.All<'TDoc>(tableName, fromDataSource ()) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields = + WithProps.Find.allOrdered<'TDoc> tableName orderFields (fromDataSource ()) + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc> tableName orderFields = + WithProps.Find.AllOrdered<'TDoc>(tableName, orderFields, fromDataSource ()) + /// Retrieve a document by its ID; returns None if not found [] let byId<'TKey, 'TDoc> tableName docId = @@ -831,13 +902,22 @@ module Find = WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) /// Retrieve documents matching a JSON field comparison query (->> =) - [] - let byField<'TDoc> tableName field = - WithProps.Find.byField<'TDoc> tableName field (fromDataSource ()) + [] + let byFields<'TDoc> tableName howMatched fields = + WithProps.Find.byFields<'TDoc> tableName howMatched fields (fromDataSource ()) /// Retrieve documents matching a JSON field comparison query (->> =) - let ByField<'TDoc>(tableName, field) = - WithProps.Find.ByField<'TDoc>(tableName, field, fromDataSource ()) + let ByFields<'TDoc>(tableName, howMatched, fields) = + WithProps.Find.ByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryField orderFields = + WithProps.Find.byFieldsOrdered<'TDoc> tableName howMatched queryField orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) = + WithProps.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) /// Retrieve documents matching a JSON containment query (@>) [] @@ -848,6 +928,15 @@ module Find = let ByContains<'TDoc>(tableName, criteria: obj) = WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ()) + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + [] + let byContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.byContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON containment query (@>) ordered by the given fields in the document + let ByContainsOrdered<'TDoc>(tableName, criteria: obj, orderFields) = + WithProps.Find.ByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) + /// Retrieve documents matching a JSON Path match query (@?) [] let byJsonPath<'TDoc> tableName jsonPath = @@ -857,14 +946,34 @@ module Find = let ByJsonPath<'TDoc>(tableName, jsonPath) = WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + [] + let byJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.byJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) ordered by the given fields in the document + let ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields) = + WithProps.Find.ByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found - [] - let firstByField<'TDoc> tableName field = - WithProps.Find.firstByField<'TDoc> tableName field (fromDataSource ()) + [] + let firstByFields<'TDoc> tableName howMatched fields = + WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ()) /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found - let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = - WithProps.Find.FirstByField<'TDoc>(tableName, field, fromDataSource ()) + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = + WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns None if not found + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithProps.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the + /// document; returns null if not found + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] @@ -875,6 +984,17 @@ module Find = let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns None if not found + [] + let firstByContainsOrdered<'TDoc> tableName (criteria: obj) orderFields = + WithProps.Find.firstByContainsOrdered<'TDoc> tableName criteria orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; + /// returns null if not found + let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields) = + WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] let firstByJsonPath<'TDoc> tableName jsonPath = @@ -884,6 +1004,17 @@ module Find = let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns None if not found + [] + let firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields = + WithProps.Find.firstByJsonPathOrdered<'TDoc> tableName jsonPath orderFields (fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; + /// returns null if not found + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields) = + WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) + /// Commands to update documents [] @@ -914,9 +1045,9 @@ module Patch = WithProps.Patch.byId tableName docId patch (fromDataSource ()) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) - [] - let byField tableName field (patch: 'TPatch) = - WithProps.Patch.byField tableName field patch (fromDataSource ()) + [] + let byFields tableName howMatched fields (patch: 'TPatch) = + WithProps.Patch.byFields tableName howMatched fields patch (fromDataSource ()) /// Patch documents using a JSON containment query in the WHERE clause (@>) [] @@ -934,42 +1065,26 @@ module Patch = module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames = WithProps.RemoveFields.byId tableName docId fieldNames (fromDataSource ()) - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames) = - WithProps.RemoveFields.ById(tableName, docId, fieldNames, fromDataSource ()) - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let byField tableName field fieldNames = - WithProps.RemoveFields.byField tableName field fieldNames (fromDataSource ()) - - /// Remove fields from documents via a comparison on a JSON field in the document - let ByField(tableName, field, fieldNames) = - WithProps.RemoveFields.ByField(tableName, field, fieldNames, fromDataSource ()) + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames = + WithProps.RemoveFields.byFields tableName howMatched fields fieldNames (fromDataSource ()) /// Remove fields from documents via a JSON containment query (@>) - [] + [] let byContains tableName (criteria: 'TContains) fieldNames = WithProps.RemoveFields.byContains tableName criteria fieldNames (fromDataSource ()) - /// Remove fields from documents via a JSON containment query (@>) - let ByContains(tableName, criteria: 'TContains, fieldNames) = - WithProps.RemoveFields.ByContains(tableName, criteria, fieldNames, fromDataSource ()) - /// Remove fields from documents via a JSON Path match query (@?) - [] + [] let byJsonPath tableName jsonPath fieldNames = WithProps.RemoveFields.byJsonPath tableName jsonPath fieldNames (fromDataSource ()) - /// Remove fields from documents via a JSON Path match query (@?) - let ByJsonPath(tableName, jsonPath, fieldNames) = - WithProps.RemoveFields.ByJsonPath(tableName, jsonPath, fieldNames, fromDataSource ()) - /// Commands to delete documents [] module Delete = @@ -978,11 +1093,11 @@ module Delete = [] let byId tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (fromDataSource ()) - + /// Delete documents by matching a JSON field comparison query (->> =) - [] - let byField tableName field = - WithProps.Delete.byField tableName field (fromDataSource ()) + [] + let byFields tableName howMatched fields = + WithProps.Delete.byFields tableName howMatched fields (fromDataSource ()) /// Delete documents by matching a JSON containment query (@>) [] diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index 7d46603..f51a328 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Sqlite/Compat.fs b/src/Sqlite/Compat.fs new file mode 100644 index 0000000..7288470 --- /dev/null +++ b/src/Sqlite/Compat.fs @@ -0,0 +1,269 @@ +namespace BitBadger.Documents.Sqlite.Compat + +open BitBadger.Documents +open BitBadger.Documents.Sqlite + +[] +module Parameters = + + /// Create a JSON field parameter + [] + [] + 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 + [] + [] + let fieldNameParam fieldNames = + fieldNameParams fieldNames + + +[] +module Query = + + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document + [] + [] + let whereByField field paramName = + Query.whereByFields Any [ { field with ParameterName = Some paramName } ] + + +module WithConn = + + [] + module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field conn = + WithConn.Count.byFields tableName Any [ field ] conn + + [] + module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field conn = + WithConn.Exists.byFields tableName Any [ field ] conn + + [] + module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field conn = + WithConn.Find.byFields<'TDoc> tableName Any [ field ] conn + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + 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 + [] + [] + 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 + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn) + + [] + module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) conn = + WithConn.Patch.byFields tableName Any [ field ] patch conn + + [] + module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames conn = + WithConn.RemoveFields.byFields tableName Any [ field ] fieldNames conn + + [] + module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field conn = + WithConn.Delete.byFields tableName Any [ field ] conn + + +[] +module Count = + + /// Count matching documents using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Count.byFields tableName Any [ field ] + + +[] +module Exists = + + /// Determine if a document exists using a JSON field comparison (->> =) + [] + [] + let byField tableName field = + Exists.byFields tableName Any [ field ] + + +[] +module Find = + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + [] + let byField<'TDoc> tableName field = + Find.byFields<'TDoc> tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + 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 + [] + [] + 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 + [] + let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) + + +[] +module Patch = + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + let byField tableName field (patch: 'TPatch) = + Patch.byFields tableName Any [ field ] patch + + +[] +module RemoveFields = + + /// Remove fields from documents via a comparison on a JSON field in the document + [] + [] + let byField tableName field fieldNames = + RemoveFields.byFields tableName Any [ field ] fieldNames + + +[] +module Delete = + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + [] + let byField tableName field = + Delete.byFields tableName Any [ field ] + + +open Microsoft.Data.Sqlite + +/// F# Extensions for the NpgsqlConnection type +[] +module Extensions = + + type SqliteConnection with + + /// Count matching documents using a JSON field comparison query (->> =) + [] + member conn.countByField tableName field = + conn.countByFields tableName Any [ field ] + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + member conn.existsByField tableName field = + conn.existsByFields tableName Any [ field ] + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + 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 + [] + member conn.findFirstByField<'TDoc> tableName field = + conn.findFirstByFields<'TDoc> tableName Any [ field ] + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + 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 + [] + member conn.removeFieldsByField tableName field fieldNames = + conn.removeFieldsByFields tableName Any [ field ] fieldNames + + /// Delete documents by matching a JSON field comparison query (->> =) + [] + 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 (->> =) + [] + [] + static member inline CountByField(conn, tableName, field) = + WithConn.Count.byFields tableName Any [ field ] conn + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + [] + static member inline ExistsByField(conn, tableName, field) = + WithConn.Exists.byFields tableName Any [ field ] conn + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + [] + 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 + [] + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = + WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn) + + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + [] + 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 + [] + [] + 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 (->> =) + [] + [] + static member inline DeleteByField(conn, tableName, field) = + WithConn.Delete.byFields tableName Any [ field ] conn diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 7ad8888..4a32dec 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -1,5 +1,6 @@ namespace BitBadger.Documents.Sqlite +open BitBadger.Documents open Microsoft.Data.Sqlite /// F# extensions for the SqliteConnection type @@ -34,44 +35,57 @@ module Extensions = /// Insert a new document member conn.insert<'TDoc> tableName (document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") member conn.save<'TDoc> tableName (document: 'TDoc) = - WithConn.save tableName document conn + WithConn.Document.save tableName document conn /// Count all documents in a table member conn.countAll tableName = WithConn.Count.all tableName conn - /// Count matching documents using a comparison on a JSON field - member conn.countByField tableName field = - WithConn.Count.byField tableName field conn + /// Count matching documents using a comparison on JSON fields + member conn.countByFields tableName howMatched fields = + WithConn.Count.byFields tableName howMatched fields conn /// Determine if a document exists for the given ID member conn.existsById tableName (docId: 'TKey) = WithConn.Exists.byId tableName docId conn - /// Determine if a document exists using a comparison on a JSON field - member conn.existsByField tableName field = - WithConn.Exists.byField tableName field conn - + /// Determine if a document exists using a comparison on JSON fields + member conn.existsByFields tableName howMatched fields = + WithConn.Exists.byFields tableName howMatched fields conn + /// Retrieve all documents in the given table member conn.findAll<'TDoc> tableName = WithConn.Find.all<'TDoc> tableName conn + /// Retrieve all documents in the given table ordered by the given fields in the document + member conn.findAllOrdered<'TDoc> tableName orderFields = + WithConn.Find.allOrdered<'TDoc> tableName orderFields conn + /// Retrieve a document by its ID member conn.findById<'TKey, 'TDoc> tableName (docId: 'TKey) = WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn - - /// Retrieve documents via a comparison on a JSON field - member conn.findByField<'TDoc> tableName field = - WithConn.Find.byField<'TDoc> tableName field 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 - + + /// Retrieve documents via a comparison on JSON fields + member conn.findByFields<'TDoc> tableName howMatched fields = + WithConn.Find.byFields<'TDoc> tableName howMatched fields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + member conn.findByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + member conn.findFirstByFields<'TDoc> tableName howMatched fields = + WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + member conn.findFirstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + /// Update an entire document by its ID member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = WithConn.Update.byId tableName docId document conn @@ -84,25 +98,25 @@ module Extensions = member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) = WithConn.Patch.byId tableName docId 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 - + /// Patch documents using a comparison on JSON fields + member conn.patchByFields tableName howMatched fields (patch: 'TPatch) = + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Remove fields from a document by the document's ID member conn.removeFieldsById tableName (docId: 'TKey) fieldNames = WithConn.RemoveFields.byId tableName docId fieldNames conn - /// Remove a field from a document via a comparison on a JSON field in the document - member conn.removeFieldsByField tableName field fieldNames = - WithConn.RemoveFields.byField tableName field fieldNames conn + /// Remove a field from a document via a comparison on JSON fields in the document + member conn.removeFieldsByFields tableName howMatched fields fieldNames = + WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn /// Delete a document by its ID member conn.deleteById tableName (docId: 'TKey) = WithConn.Delete.byId tableName docId conn - /// Delete documents by matching a comparison on a JSON field - member conn.deleteByField tableName field = - WithConn.Delete.byField tableName field conn + /// Delete documents by matching a comparison on JSON fields + member conn.deleteByFields tableName howMatched fields = + WithConn.Delete.byFields tableName howMatched fields conn open System.Runtime.CompilerServices @@ -145,53 +159,70 @@ type SqliteConnectionCSharpExtensions = /// Insert a new document [] static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.save<'TDoc> tableName document conn + WithConn.Document.save<'TDoc> tableName document conn /// Count all documents in a table [] static member inline CountAll(conn, tableName) = WithConn.Count.all tableName conn - /// Count matching documents using a comparison on a JSON field + /// Count matching documents using a comparison on JSON fields [] - static member inline CountByField(conn, tableName, field) = - WithConn.Count.byField tableName field conn - + static member inline CountByFields(conn, tableName, howMatched, fields) = + WithConn.Count.byFields tableName howMatched fields conn + /// Determine if a document exists for the given ID [] static member inline ExistsById<'TKey>(conn, tableName, docId: 'TKey) = WithConn.Exists.byId tableName docId conn - /// Determine if a document exists using a comparison on a JSON field + /// Determine if a document exists using a comparison on JSON fields [] - static member inline ExistsByField(conn, tableName, field) = - WithConn.Exists.byField tableName field conn + static member inline ExistsByFields(conn, tableName, howMatched, fields) = + WithConn.Exists.byFields tableName howMatched fields conn /// Retrieve all documents in the given table [] static member inline FindAll<'TDoc>(conn, tableName) = WithConn.Find.All<'TDoc>(tableName, conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + static member inline FindAllOrdered<'TDoc>(conn, tableName, orderFields) = + WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn) + /// Retrieve a document by its ID [] static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) - /// Retrieve documents via a comparison on a JSON field + /// Retrieve documents via a comparison on JSON fields [] - static member inline FindByField<'TDoc>(conn, tableName, field) = - WithConn.Find.ByField<'TDoc>(tableName, field, conn) - - /// Retrieve documents via a comparison on a JSON field, returning only the first result + static member inline FindByFields<'TDoc>(conn, tableName, howMatched, fields) = + WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = - WithConn.Find.FirstByField<'TDoc>(tableName, field, conn) - + static member inline FindByFieldsOrdered<'TDoc>(conn, tableName, howMatched, queryFields, orderFields) = + WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + [] + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + conn, tableName, howMatched, queryFields, orderFields) = + WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + /// Update an entire document by its ID [] static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = @@ -207,27 +238,33 @@ type SqliteConnectionCSharpExtensions = static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) = WithConn.Patch.byId tableName docId patch conn - /// Patch documents using a comparison on a JSON field + /// Patch documents using a comparison on JSON fields [] - static member inline PatchByField<'TPatch>(conn, tableName, field, patch: 'TPatch) = - WithConn.Patch.byField tableName field patch conn - + static member inline PatchByFields<'TPatch>(conn, tableName, howMatched, fields, patch: 'TPatch) = + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Remove fields from a document by the document's ID [] static member inline RemoveFieldsById<'TKey>(conn, tableName, docId: 'TKey, fieldNames) = - WithConn.RemoveFields.ById(tableName, docId, fieldNames, conn) - - /// Remove fields from documents via a comparison on a JSON field in the document + WithConn.RemoveFields.byId tableName docId fieldNames conn + + /// Remove fields from documents via a comparison on JSON fields in the document [] - static member inline RemoveFieldsByField(conn, tableName, field, fieldNames) = - WithConn.RemoveFields.ByField(tableName, field, fieldNames, conn) + static member inline RemoveFieldsByFields(conn, tableName, howMatched, fields, fieldNames) = + WithConn.RemoveFields.byFields tableName howMatched fields fieldNames conn /// Delete a document by its ID [] static member inline DeleteById<'TKey>(conn, tableName, docId: 'TKey) = WithConn.Delete.byId tableName docId conn - + + /// Delete documents by matching a comparison on JSON fields + [] + static member inline DeleteByFields(conn, tableName, howMatched, fields) = + WithConn.Delete.byFields tableName howMatched fields conn + /// Delete documents by matching a comparison on a JSON field [] + [] static member inline DeleteByField(conn, tableName, field) = - WithConn.Delete.byField tableName field conn + conn.DeleteByFields(tableName, Any, [ field ]) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 27815f8..d34d3f1 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -31,20 +31,47 @@ module Configuration = [] module Query = - /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document - [] - let whereByField field paramName = - let theRest = - match field.Op with - | EX | NEX -> "" - | BT -> $" {paramName}min AND {paramName}max" - | _ -> $" %s{paramName}" - $"data->>'{field.Name}' {field.Op}{theRest}" - + /// Create a WHERE clause fragment to implement a comparison on fields in a JSON document + [] + let whereByFields (howMatched: FieldMatch) fields = + let name = ParameterName() + fields + |> Seq.map (fun it -> + match it.Op with + | EX | NEX -> $"{it.Path SQLite} {it.Op}" + | BT -> + let p = name.Derive it.ParameterName + $"{it.Path SQLite} {it.Op} {p}min AND {p}max" + | _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}") + |> String.concat $" {howMatched} " + /// Create a WHERE clause fragment to implement an ID-based query [] let whereById paramName = - whereByField (Field.EQ (Configuration.idField ()) 0) paramName + whereByFields Any [ { Field.EQ (Configuration.idField ()) 0 with ParameterName = Some paramName } ] + + /// Create an UPDATE statement to patch documents + [] + let patch tableName = + $"UPDATE %s{tableName} SET data = json_patch(data, json(@data))" + + /// Create an UPDATE statement to remove fields from documents + [] + let removeFields tableName (parameters: SqliteParameter seq) = + let paramNames = parameters |> Seq.map _.ParameterName |> String.concat ", " + $"UPDATE %s{tableName} SET data = json_remove(data, {paramNames})" + + /// Create a query by a document's ID + [] + let byId<'TKey> statement (docId: 'TKey) = + Query.statementWhere + statement + (whereByFields Any [ { Field.EQ (Configuration.idField ()) docId with ParameterName = Some "@id" } ]) + + /// Create a query on JSON fields + [] + let byFields statement howMatched fields = + Query.statementWhere statement (whereByFields howMatched fields) /// Data definition module Definition = @@ -53,107 +80,7 @@ module Query = [] let ensureTable name = Query.Definition.ensureTableFor name "TEXT" - - /// Query to update a document - [] - let update tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Queries for counting documents - module Count = - - /// Query to count all documents in a table - [] - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - - /// Query to count matching documents using a text comparison on a JSON field - [] - 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 - [] - 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 - [] - 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 - [] - let byId tableName = - $"""{Query.selectFromTable tableName} WHERE {whereById "@id"}""" - - /// Query to retrieve documents using a comparison on a JSON field - [] - 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 - [] - let byId tableName = - whereById "@id" |> update tableName - - /// Query to patch (partially update) a document via a comparison on a JSON field - [] - 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 - [] - 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 - [] - 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 - [] - let byId tableName = - $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - - /// Query to delete documents using a comparison on a JSON field - [] - let byField tableName field = - $"""DELETE FROM %s{tableName} WHERE {whereByField field "@field"}""" - /// Parameter handling helpers [] @@ -169,39 +96,39 @@ module Parameters = let jsonParam name (it: 'TJson) = SqliteParameter(name, Configuration.serializer().Serialize it) + /// Create JSON field parameters + [] + let addFieldParams fields parameters = + let name = ParameterName() + fields + |> Seq.map (fun it -> + seq { + match it.Op with + | EX | NEX -> () + | BT -> + let p = name.Derive it.ParameterName + let values = it.Value :?> obj list + yield SqliteParameter($"{p}min", List.head values) + yield SqliteParameter($"{p}max", List.last values) + | _ -> yield SqliteParameter(name.Derive it.ParameterName, it.Value) }) + |> Seq.collect id + |> Seq.append parameters + |> Seq.toList + |> Seq.ofList + /// Create a JSON field parameter (name "@field") - [] + [] + [] 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 + addFieldParams [ { field with ParameterName = Some name } ] parameters - /// Create a JSON field parameter (name "@field") - let AddField(name, field, parameters) = - match field.Op with - | EX | NEX -> parameters - | BT -> - let values = field.Value :?> obj list - // let min = SqliteParameter($"{name}min", SqliteType.Integer) - // min.Value <- values[0] - // let max = SqliteParameter($"{name}max", SqliteType.Integer) - // max.Value <- values[1] - [ SqliteParameter($"{name}min", values[0]); SqliteParameter($"{name}max", values[1]) ] - // [min; max] - |> Seq.append parameters - | _ -> SqliteParameter(name, field.Value) |> Seq.singleton |> Seq.append parameters - /// Append JSON field name parameters for the given field names to the given parameters - [] - let fieldNameParams paramName (fieldNames: string list) = - fieldNames |> List.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}")) - - /// Append JSON field name parameters for the given field names to the given parameters - let FieldNames(paramName, fieldNames: string seq) = - fieldNames |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.{name}")) + [] + let fieldNameParams paramName fieldNames = + fieldNames + |> Seq.mapi (fun idx name -> SqliteParameter($"%s{paramName}{idx}", $"$.%s{name}")) + |> Seq.toList + |> Seq.ofList /// An empty parameter sequence [] @@ -323,23 +250,42 @@ module WithConn = [] let ensureTable name conn = backgroundTask { do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn - do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn + do! Custom.nonQuery (Query.Definition.ensureKey name SQLite) [] conn } /// Create an index on a document table [] let ensureFieldIndex tableName indexName fields conn = - Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn + Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn - /// Insert a new document - [] - 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") - [] - let save<'TDoc> tableName (document: 'TDoc) conn = - Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn + /// Commands to add documents + [] + module Document = + + /// Insert a new document + [] + 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 + + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save<'TDoc> tableName (document: 'TDoc) conn = + Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn /// Commands to count documents [] @@ -348,12 +294,13 @@ module WithConn = /// Count all documents in a table [] let all tableName conn = - Custom.scalar (Query.Count.all tableName) [] toCount conn + Custom.scalar (Query.count tableName) [] toCount conn - /// Count matching documents using a comparison on a JSON field - [] - let byField tableName field conn = - Custom.scalar (Query.Count.byField tableName field) (addFieldParam "@field" field []) toCount conn + /// Count matching documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.scalar + (Query.byFields (Query.count tableName) howMatched fields) (addFieldParams fields []) toCount conn /// Commands to determine if documents exist [] @@ -362,12 +309,16 @@ module WithConn = /// Determine if a document exists for the given ID [] 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 - [] - let byField tableName field conn = - Custom.scalar (Query.Exists.byField tableName field) (addFieldParam "@field" field []) toExists conn + Custom.scalar (Query.exists tableName (Query.whereById "@id")) [ idParam docId ] toExists conn + + /// Determine if a document exists using a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.scalar + (Query.exists tableName (Query.whereByFields howMatched fields)) + (addFieldParams fields []) + toExists + conn /// Commands to retrieve documents [] @@ -376,42 +327,99 @@ module WithConn = /// Retrieve all documents in the given table [] let all<'TDoc> tableName conn = - Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn + Custom.list<'TDoc> (Query.find tableName) [] fromData<'TDoc> conn /// Retrieve all documents in the given table let All<'TDoc>(tableName, conn) = - Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn) + Custom.List(Query.find tableName, [], fromData<'TDoc>, conn) + + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields conn = + Custom.list<'TDoc> (Query.find tableName + Query.orderBy orderFields SQLite) [] fromData<'TDoc> conn + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc>(tableName, orderFields, conn) = + Custom.List(Query.find tableName + Query.orderBy orderFields SQLite, [], fromData<'TDoc>, conn) /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn = - Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn + Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn /// Retrieve a document by its ID (returns null if not found) let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = - Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn) - - /// Retrieve documents via a comparison on a JSON field - [] - let byField<'TDoc> tableName field conn = + Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn) + + /// Retrieve documents via a comparison on JSON fields + [] + let byFields<'TDoc> tableName howMatched fields conn = Custom.list<'TDoc> - (Query.Find.byField tableName field) (addFieldParam "@field" field []) fromData<'TDoc> conn + (Query.byFields (Query.find tableName) howMatched fields) + (addFieldParams fields []) + fromData<'TDoc> + conn - /// Retrieve documents via a comparison on a JSON field - let ByField<'TDoc>(tableName, field, conn) = + /// Retrieve documents via a comparison on JSON fields + let ByFields<'TDoc>(tableName, howMatched, fields, conn) = Custom.List<'TDoc>( - Query.Find.byField tableName field, addFieldParam "@field" field [], fromData<'TDoc>, conn) + Query.byFields (Query.find tableName) howMatched fields, + addFieldParams fields [], + fromData<'TDoc>, + conn) - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - let firstByField<'TDoc> tableName field conn = + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + [] + 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 + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + 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) + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + let firstByFields<'TDoc> tableName howMatched fields conn = Custom.single - $"{Query.Find.byField tableName field} LIMIT 1" (addFieldParam "@field" field []) fromData<'TDoc> conn + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1" + (addFieldParams fields []) + fromData<'TDoc> + conn - /// Retrieve documents via a comparison on a JSON field, returning only the first result - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + /// Retrieve documents via a comparison on JSON fields, returning only the first result + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = Custom.Single( - $"{Query.Find.byField tableName field} LIMIT 1", addFieldParam "@field" field [], fromData<'TDoc>, conn) + $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", + addFieldParams fields [], + fromData<'TDoc>, + conn) + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + [] + 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 + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning + /// only the first result + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(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) /// Commands to update documents [] @@ -420,7 +428,10 @@ module WithConn = /// Update an entire document by its ID [] let byId tableName (docId: 'TKey) (document: 'TDoc) conn = - Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn + Custom.nonQuery + (Query.statementWhere (Query.update tableName) (Query.whereById "@id")) + [ idParam docId; jsonParam "@data" document ] + conn /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] @@ -438,38 +449,38 @@ module WithConn = /// Patch a document by its ID [] 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 - [] - let byField tableName field (patch: 'TPatch) (conn: SqliteConnection) = Custom.nonQuery - (Query.Patch.byField tableName field) (addFieldParam "@field" field [ jsonParam "@data" patch ]) conn - + (Query.byId (Query.patch tableName) docId) [ idParam docId; jsonParam "@data" patch ] conn + + /// Patch documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields (patch: 'TPatch) conn = + Custom.nonQuery + (Query.byFields (Query.patch tableName) howMatched fields) + (addFieldParams fields [ jsonParam "@data" patch ]) + conn + /// Commands to remove fields from documents [] module RemoveFields = /// Remove fields from a document by the document's ID - [] + [] let byId tableName (docId: 'TKey) fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames - Custom.nonQuery (Query.RemoveFields.byId tableName nameParams) (idParam docId :: nameParams) conn + Custom.nonQuery + (Query.byId (Query.removeFields tableName nameParams) docId) + (idParam docId |> Seq.singleton |> Seq.append nameParams) + conn - /// Remove fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames, conn) = - byId tableName docId (List.ofSeq fieldNames) conn - - /// Remove fields from documents via a comparison on a JSON field in the document - [] - let byField tableName field fieldNames conn = + /// Remove fields from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields fieldNames conn = let nameParams = fieldNameParams "@name" fieldNames Custom.nonQuery - (Query.RemoveFields.byField tableName field nameParams) (addFieldParam "@field" field nameParams) conn - - /// 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 + (Query.byFields (Query.removeFields tableName nameParams) howMatched fields) + (addFieldParams fields nameParams) + conn /// Commands to delete documents [] @@ -478,12 +489,12 @@ module WithConn = /// Delete a document by its ID [] 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 - [] - let byField tableName field conn = - Custom.nonQuery (Query.Delete.byField tableName field) (addFieldParam "@field" field []) conn + Custom.nonQuery (Query.byId (Query.delete tableName) docId) [ idParam docId ] conn + + /// Delete documents by matching a comparison on JSON fields + [] + let byFields tableName howMatched fields conn = + Custom.nonQuery (Query.byFields (Query.delete tableName) howMatched fields) (addFieldParams fields []) conn /// Commands to execute custom SQL queries @@ -529,6 +540,7 @@ module Custom = use conn = Configuration.dbConn () WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) + /// Functions to create tables and indexes [] module Definition = @@ -545,6 +557,7 @@ module Definition = use conn = Configuration.dbConn () WithConn.Definition.ensureFieldIndex tableName indexName fields conn + /// Document insert/save functions [] module Document = @@ -553,13 +566,14 @@ module Document = [] let insert<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.insert tableName document conn + WithConn.Document.insert tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] let save<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.save tableName document conn + WithConn.Document.save tableName document conn + /// Commands to count documents [] @@ -571,11 +585,12 @@ module Count = use conn = Configuration.dbConn () WithConn.Count.all tableName conn - /// Count matching documents using a comparison on a JSON field - [] - let byField tableName field = + /// Count matching documents using a comparison on JSON fields + [] + let byFields tableName howMatched fields = use conn = Configuration.dbConn () - WithConn.Count.byField tableName field conn + WithConn.Count.byFields tableName howMatched fields conn + /// Commands to determine if documents exist [] @@ -586,12 +601,13 @@ module Exists = 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 - [] - let byField tableName field = + + /// Determine if a document exists using a comparison on JSON fields + [] + let byFields tableName howMatched fields = use conn = Configuration.dbConn () - WithConn.Exists.byField tableName field conn + WithConn.Exists.byFields tableName howMatched fields conn + /// Commands to determine if documents exist [] @@ -608,6 +624,17 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.All<'TDoc>(tableName, conn) + /// Retrieve all documents in the given table ordered by the given fields in the document + [] + let allOrdered<'TDoc> tableName orderFields = + use conn = Configuration.dbConn () + WithConn.Find.allOrdered<'TDoc> tableName orderFields conn + + /// Retrieve all documents in the given table ordered by the given fields in the document + let AllOrdered<'TDoc> tableName orderFields = + use conn = Configuration.dbConn () + WithConn.Find.AllOrdered<'TDoc>(tableName, orderFields, conn) + /// Retrieve a document by its ID (returns None if not found) [] let byId<'TKey, 'TDoc> tableName docId = @@ -619,27 +646,52 @@ module Find = use conn = Configuration.dbConn () WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) - /// Retrieve documents via a comparison on a JSON field - [] - let byField<'TDoc> tableName field = + /// Retrieve documents via a comparison on JSON fields + [] + let byFields<'TDoc> tableName howMatched fields = use conn = Configuration.dbConn () - WithConn.Find.byField<'TDoc> tableName field conn + WithConn.Find.byFields<'TDoc> tableName howMatched fields conn + + /// Retrieve documents via a comparison on JSON fields + let ByFields<'TDoc>(tableName, howMatched, fields) = + use conn = Configuration.dbConn () + WithConn.Find.ByFields<'TDoc>(tableName, howMatched, fields, conn) + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + [] + let byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = + use conn = Configuration.dbConn () + WithConn.Find.byFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document + let ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields) = + use conn = Configuration.dbConn () + WithConn.Find.ByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + [] + let firstByFields<'TDoc> tableName howMatched fields = + use conn = Configuration.dbConn () + WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn + + /// Retrieve documents via a comparison on JSON fields, returning only the first result + let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = + use conn = Configuration.dbConn () + WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) - /// Retrieve documents via a comparison on a JSON field - let ByField<'TDoc>(tableName, field) = + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + [] + let firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields = use conn = Configuration.dbConn () - WithConn.Find.ByField<'TDoc>(tableName, field, conn) + WithConn.Find.firstByFieldsOrdered<'TDoc> tableName howMatched queryFields orderFields conn + + /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only + /// the first result + let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + use conn = Configuration.dbConn () + WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - 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 [] @@ -662,6 +714,7 @@ module Update = use conn = Configuration.dbConn () WithConn.Update.ByFunc(tableName, idFunc, document, conn) + /// Commands to patch (partially update) documents [] module Patch = @@ -672,37 +725,29 @@ module Patch = use conn = Configuration.dbConn () WithConn.Patch.byId tableName docId patch conn - /// Patch documents using a comparison on a JSON field in the WHERE clause - [] - let byField tableName field (patch: 'TPatch) = + /// Patch documents using a comparison on JSON fields in the WHERE clause + [] + let byFields tableName howMatched fields (patch: 'TPatch) = use conn = Configuration.dbConn () - WithConn.Patch.byField tableName field patch conn + WithConn.Patch.byFields tableName howMatched fields patch conn + /// Commands to remove fields from documents [] module RemoveFields = /// 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 fields from a document by the document's ID - let ById(tableName, docId: 'TKey, fieldNames) = + + /// Remove field from documents via a comparison on JSON fields in the document + [] + let byFields tableName howMatched fields 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 - [] - let byField tableName field fieldNames = - use conn = Configuration.dbConn () - WithConn.RemoveFields.byField tableName field fieldNames conn + WithConn.RemoveFields.byFields tableName howMatched fields 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 [] @@ -713,9 +758,9 @@ module Delete = 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 - [] - let byField tableName field = + + /// Delete documents by matching a comparison on JSON fields + [] + let byFields tableName howMatched fields = use conn = Configuration.dbConn () - WithConn.Delete.byField tableName field conn + WithConn.Delete.byFields tableName howMatched fields conn diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index 951b6d7..b231560 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -3,6 +3,7 @@ enable enable + latest diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index fd8162a..0a0ca67 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -1,6 +1,7 @@ using Expecto.CSharp; using Expecto; using Microsoft.FSharp.Collections; +using Microsoft.FSharp.Core; namespace BitBadger.Documents.Tests.CSharp; @@ -21,209 +22,613 @@ internal class TestSerializer : IDocumentSerializer public static class CommonCSharpTests { /// - /// Unit tests + /// Unit tests for the Op enum /// - [Tests] - public static readonly Test Unit = TestList("Common.C# Unit", new[] - { - TestSequenced( - TestList("Configuration", new[] + private static readonly Test OpTests = TestList("Op", + [ + TestCase("EQ succeeds", () => + { + Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); + }), + 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"); + }) + ]); + + /// + /// Unit tests for the Field class + /// + private static readonly Test FieldTests = TestList("Field", + [ + 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)field.Value).ToArray(), [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("NameToPath", + [ + TestCase("succeeds for PostgreSQL and a simple name", () => { - TestCase("UseSerializer succeeds", () => - { - try - { - Configuration.UseSerializer(new TestSerializer()); + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), + "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), + "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), + "Path not constructed correctly"); + }) + ]), + TestCase("WithParameterName succeeds", () => + { + var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); + Expect.isSome(field.ParameterName, "The parameter name should have been filled"); + Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect"); + }), + TestCase("WithQualifier succeeds", () => + { + var field = Field.EQ("Bill", "Matt").WithQualifier("joe"); + Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); + Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); + }), + TestList("Path", + [ + TestCase("succeeds for a PostgreSQL single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }) + ]) + ]); - var serialized = Configuration.Serializer().Serialize(new SubDocument - { - Foo = "howdy", - Bar = "bye" - }); - Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); + /// + /// Unit tests for the FieldMatch enum + /// + 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"); + }) + ]); - var deserialized = Configuration.Serializer() - .Deserialize("{\"Something\":\"here\"}"); - Expect.isNull(deserialized, "Specified serializer should have returned null"); - } - finally - { - Configuration.UseSerializer(DocumentSerializer.Default); - } - }), - TestCase("Serializer returns configured serializer", () => + /// + /// Unit tests for the ParameterName class + /// + private static readonly Test ParameterNameTests = TestList("ParameterName.Derive", + [ + TestCase("succeeds with existing name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); + Expect.equal(name.Derive(FSharpOption.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.None), "@field0", + "Anonymous field name should have been returned"); + Expect.equal(name.Derive(FSharpOption.None), "@field1", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field2", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field3", + "Counter should have advanced from previous call"); + }) + ]); + + /// + /// Unit tests for the AutoId enum + /// + 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 { - Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), - "Serializer should have been the same"); - }), - TestCase("UseIdField / IdField succeeds", () => + _ = AutoId.NeedsAutoId(AutoId.Number, new { Key = "" }, "Id"); + Expect.isTrue(false, "Non-existent ID property should have thrown an exception"); + } + catch (InvalidOperationException) { - 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"); - } + // 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 + } + }) + ]) + ]); + + /// + /// Unit tests for the Configuration static class + /// + private static readonly Test ConfigurationTests = TestList("Configuration", + [ + TestCase("UseSerializer succeeds", () => + { + try + { + Configuration.UseSerializer(new TestSerializer()); + + var serialized = Configuration.Serializer().Serialize(new SubDocument + { + Foo = "howdy", + Bar = "bye" + }); + Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); + + var deserialized = Configuration.Serializer() + .Deserialize("{\"Something\":\"here\"}"); + Expect.isNull(deserialized, "Specified serializer should have returned null"); + } + finally + { + Configuration.UseSerializer(DocumentSerializer.Default); + } + }), + TestCase("Serializer returns configured serializer", () => + { + Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), + "Serializer should have been the same"); + }), + TestCase("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"); + } + }), + 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); + } + }) + ]); + + /// + /// Unit tests for the Query static class + /// + private static readonly Test QueryTests = TestList("Query", + [ + TestCase("StatementWhere succeeds", () => + { + Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); + }), + TestList("Definition", + [ + TestCase("EnsureTableFor succeeds", () => + { + Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), + "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + "CREATE TABLE statement not constructed correctly"); + }), + TestList("EnsureKey", + [ + 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'))", + "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'))", + "CREATE INDEX for key statement without schema not constructed correctly"); }) - })), - TestList("Op", new[] - { - TestCase("EQ succeeds", () => - { - Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); - }), - 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)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", new[] - { - TestCase("succeeds when a schema is present", () => - { - 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"), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))", - "CREATE INDEX for key statement without schema not constructed correctly"); - }) - }), - TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => + ]), + TestList("EnsureIndexOn", + [ + TestCase("succeeds for multiple fields and directions", () => { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", - new[] { "taco", "guac DESC", "salsa ASC" }), + ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + + "((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", () => + ]) + ]), + TestCase("Insert succeeds", () => + { + Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + }), + TestCase("Save succeeds", () => + { + 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.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + 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("Save succeeds", () => + TestCase("succeeds for PostgreSQL with one field and no direction", () => { - 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"); + 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"); }) - }) - }); + ]) + ]); + + /// + /// Unit tests + /// + [Tests] + public static readonly Test Unit = TestList("Common.C# Unit", + [ + OpTests, + FieldTests, + FieldMatchTests, + ParameterNameTests, + AutoIdTests, + QueryTests, + TestSequenced(ConfigurationTests) + ]); } diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs index b4afe7f..054143a 100644 --- a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -31,17 +31,17 @@ public class PostgresCSharpExtensionTests /// Integration tests for the SQLite extension methods /// [Tests] - public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[] - { - TestList("CustomList", new[] - { + public static readonly Test Integration = TestList("Postgres.C#.Extensions", + [ + TestList("CustomList", + [ TestCase("succeeds when data is found", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.CustomList(Query.SelectFromTable(PostgresDb.TableName), Parameters.None, + var docs = await conn.CustomList(Query.Find(PostgresDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), @@ -53,13 +53,13 @@ public class PostgresCSharpExtensionTests var docs = await conn.CustomList( $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }, + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))], Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("CustomSingle", new[] - { + ]), + TestList("CustomSingle", + [ TestCase("succeeds when a row is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -67,7 +67,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData); + [Tuple.Create("@id", Sql.@string("one"))], Results.FromData); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc.Id, "one", "The incorrect document was returned"); }), @@ -78,12 +78,12 @@ public class PostgresCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); + [Tuple.Create("@id", Sql.@string("eighty"))], Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("CustomNonQuery", new[] - { + ]), + TestList("CustomNonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = PostgresDb.BuildDb(); @@ -102,12 +102,12 @@ public class PostgresCSharpExtensionTests await LoadDocs(); await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }); + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); }) - }), + ]), TestCase("Scalar succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -119,58 +119,64 @@ public class PostgresCSharpExtensionTests { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); - var tableExists = () => conn.CustomScalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, - Results.ToExists); - var keyExists = () => conn.CustomScalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None, - Results.ToExists); - var exists = await tableExists(); - var alsoExists = await keyExists(); + var exists = await TableExists(); + var alsoExists = await KeyExists(); Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already"); await conn.EnsureTable("ensured"); - exists = await tableExists(); - alsoExists = await keyExists(); + exists = await TableExists(); + alsoExists = await KeyExists(); Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + Task KeyExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", + Parameters.None, Results.ToExists); + Task TableExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", + Parameters.None, Results.ToExists); }), TestCase("EnsureDocumentIndex succeeds", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); - var indexExists = () => conn.CustomScalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", Parameters.None, - Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await conn.EnsureTable("ensured"); await conn.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); - exists = await indexExists(); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", + Parameters.None, Results.ToExists); }), TestCase("EnsureFieldIndex succeeds", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); - var indexExists = () => conn.CustomScalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None, - Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await conn.EnsureTable("ensured"); - await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); }), - TestList("Insert", new[] - { + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -198,9 +204,9 @@ public class PostgresCSharpExtensionTests // This is what should have happened } }) - }), - TestList("save", new[] - { + ]), + TestList("save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = PostgresDb.BuildDb(); @@ -230,7 +236,7 @@ public class PostgresCSharpExtensionTests Expect.isNotNull(after, "There should have been a document returned post-update"); Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); }) - }), + ]), TestCase("CountAll succeeds", async () => { await using var db = PostgresDb.BuildDb(); @@ -240,13 +246,14 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountAll(PostgresDb.TableName); Expect.equal(theCount, 5, "There should have been 5 matching documents"); }), - TestCase("CountByField succeeds", async () => + TestCase("CountByFields succeeds", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var theCount = await conn.CountByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + var theCount = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); Expect.equal(theCount, 2, "There should have been 2 matching documents"); }), TestCase("CountByContains succeeds", async () => @@ -267,8 +274,8 @@ public class PostgresCSharpExtensionTests var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); Expect.equal(theCount, 3, "There should have been 3 matching documents"); }), - TestList("ExistsById", new[] - { + TestList("ExistsById", + [ TestCase("succeeds when a document exists", async () => { await using var db = PostgresDb.BuildDb(); @@ -287,16 +294,16 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsById(PostgresDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ExistsByField", new[] - { + ]), + TestList("ExistsByField", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EX("Sub")); + var exists = await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EX("Sub")]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when documents do not exist", async () => @@ -305,12 +312,13 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var exists = await conn.ExistsByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); + var exists = + await conn.ExistsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]); Expect.isFalse(exists, "There should not have been existing documents"); }) - }), - TestList("ExistsByContains", new[] - { + ]), + TestList("ExistsByContains", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -329,9 +337,9 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" }); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("ExistsByJsonPath", new[] - { + ]), + TestList("ExistsByJsonPath", + [ TestCase("succeeds when documents exist", async () => { await using var db = PostgresDb.BuildDb(); @@ -350,9 +358,9 @@ public class PostgresCSharpExtensionTests var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("FindAll", new[] - { + ]), + TestList("FindAll", + [ TestCase("succeeds when there is data", async () => { await using var db = PostgresDb.BuildDb(); @@ -372,9 +380,47 @@ public class PostgresCSharpExtensionTests var results = await conn.FindAll(PostgresDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("FindById", new[] - { + ]), + TestList("FindAllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var results = await conn.FindAllOrdered(PostgresDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("FindById", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -394,16 +440,17 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindById(PostgresDb.TableName, "three hundred eighty-seven"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindByField", new[] - { + ]), + TestList("FindByFields", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.FindByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var docs = await conn.FindByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.equal(docs.Count, 1, "There should have been one document returned"); }), TestCase("succeeds when documents are not found", async () => @@ -412,12 +459,40 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var docs = await conn.FindByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); + var docs = await conn.FindByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindByContains", new[] - { + ]), + TestList("FindByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("FindByContains", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -437,9 +512,37 @@ public class PostgresCSharpExtensionTests var docs = await conn.FindByContains(PostgresDb.TableName, new { Value = "mauve" }); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindByJsonPath", new[] - { + ]), + TestList("FindByContainsOrdered", + [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|four", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar DESC")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|two", + "Documents not ordered correctly"); + }) + ]), + TestList("FindByJsonPath", + [ TestCase("succeeds when documents are found", async () => { await using var db = PostgresDb.BuildDb(); @@ -458,16 +561,45 @@ public class PostgresCSharpExtensionTests var docs = await conn.FindByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindFirstByField", new[] - { + ]), + TestList("FindByJsonPathOrdered", + [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "one|three|two", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue DESC")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|three|one", + "Documents not ordered correctly"); + }) + ]), + TestList("FindFirstByFields", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "another")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc.Id, "two", "The incorrect document was returned"); }), @@ -477,9 +609,10 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "purple")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -487,12 +620,38 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - var doc = await conn.FindFirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); + var doc = await conn.FindFirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindFirstByContains", new[] - { + ]), + TestList("FindFirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("five", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("FindFirstByContains", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -512,7 +671,7 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByContains(PostgresDb.TableName, new { Sub = new { Foo = "green" } }); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -523,9 +682,34 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByContains(PostgresDb.TableName, new { Value = "absent" }); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindFirstByJsonPath", new[] - { + ]), + TestList("FindFirstByContainsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("FindFirstByJsonPath", + [ TestCase("succeeds when a document is found", async () => { await using var db = PostgresDb.BuildDb(); @@ -546,7 +730,7 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -557,9 +741,34 @@ public class PostgresCSharpExtensionTests var doc = await conn.FindFirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("UpdateById", new[] - { + ]), + TestList("FindFirstByJsonPathOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("UpdateById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -588,9 +797,9 @@ public class PostgresCSharpExtensionTests await conn.UpdateById(PostgresDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("UpdateByFunc", new[] - { + ]), + TestList("UpdateByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -617,9 +826,9 @@ public class PostgresCSharpExtensionTests await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - TestList("PatchById", new[] - { + ]), + TestList("PatchById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -641,17 +850,19 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("PatchByField", new[] - { + ]), + TestList("PatchByFields", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await conn.CountByField(PostgresDb.TableName, Field.EQ("NumValue", "77")); + await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await conn.CountByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("NumValue", "77")]); Expect.equal(after, 2, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -662,11 +873,12 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.PatchByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await conn.PatchByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) - }), - TestList("PatchByContains", new[] - { + ]), + TestList("PatchByContains", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -687,9 +899,9 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); }) - }), - TestList("PatchByJsonPath", new[] - { + ]), + TestList("PatchByJsonPath", + [ TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); @@ -710,16 +922,16 @@ public class PostgresCSharpExtensionTests // This not raising an exception is the test await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); }) - }), - TestList("RemoveFieldsById", new[] - { + ]), + TestList("RemoveFieldsById", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub", "Value" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -731,7 +943,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Sub" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -744,7 +956,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["AFieldThatIsNotThere"]); }), TestCase("succeeds when no document is matched", async () => { @@ -752,19 +964,19 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsById(PostgresDb.TableName, "two", new[] { "Value" }); + await conn.RemoveFieldsById(PostgresDb.TableName, "two", ["Value"]); }) - }), - TestList("RemoveFieldsByField", new[] - { + ]), + TestList("RemoveFieldsByFields", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -776,7 +988,8 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Sub" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -789,7 +1002,8 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Nothing" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -797,20 +1011,19 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), - new[] { "Value" }); + await conn.RemoveFieldsByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.NE("Abracadabra", "apple")], ["Value"]); }) - }), - TestList("RemoveFieldsByContains", new[] - { + ]), + TestList("RemoveFieldsByContains", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -822,7 +1035,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Sub" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -835,7 +1048,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Nothing" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -843,20 +1056,18 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, - new[] { "Value" }); + await conn.RemoveFieldsByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, ["Value"]); }) - }), - TestList("RemoveFieldsByJsonPath", new[] - { + ]), + TestList("RemoveFieldsByJsonPath", + [ TestCase("succeeds when multiple fields are removed", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", - new[] { "Sub", "Value" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub", "Value"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -868,7 +1079,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Sub" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub"]); var updated = await Find.ById(PostgresDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.notEqual(updated.Value, "", "The string value should not have been removed"); @@ -881,7 +1092,7 @@ public class PostgresCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Nothing" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -889,12 +1100,11 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); // This not raising an exception is the test - await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", - new[] { "Value" }); + await conn.RemoveFieldsByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", ["Value"]); }) - }), - TestList("DeleteById", new[] - { + ]), + TestList("DeleteById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -915,16 +1125,16 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByField", new[] - { + ]), + TestList("DeleteByFields", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); await using var conn = MkConn(db); await LoadDocs(); - await conn.DeleteByField(PostgresDb.TableName, Field.NE("Value", "purple")); + await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 2, "There should have been 2 documents remaining"); }), @@ -934,13 +1144,13 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.DeleteByField(PostgresDb.TableName, Field.EQ("Value", "crimson")); + await conn.DeleteByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByContains", new[] - { + ]), + TestList("DeleteByContains", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -961,9 +1171,9 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByJsonPath", new[] - { + ]), + TestList("DeleteByJsonPath", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = PostgresDb.BuildDb(); @@ -984,6 +1194,6 @@ public class PostgresCSharpExtensionTests var remaining = await conn.CountAll(PostgresDb.TableName); Expect.equal(remaining, 5, "There should have been 5 documents remaining"); }) - }), - }); + ]), + ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 9ddd720..a224094 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -14,292 +14,314 @@ using static Runner; public static class PostgresCSharpTests { /// - /// Tests which do not hit the database + /// Unit tests for the Parameters module of the PostgreSQL library /// - private static readonly Test Unit = TestList("Unit", new[] - { - TestList("Parameters", new[] - { - TestCase("Id succeeds", () => + private static readonly Test ParametersTests = TestList("Parameters", + [ + TestList("Id", + [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + TestCase("succeeds for byte ID", () => + { + var it = Parameters.Id((sbyte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned byte ID", () => + { + var it = Parameters.Id((byte)7); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int8(7), "Unsigned byte ID parameter not constructed correctly"); + }), + TestCase("succeeds for short ID", () => + { + var it = Parameters.Id((short)44); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(44), "Short ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned short ID", () => + { + var it = Parameters.Id((ushort)64); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int16(64), "Unsigned short ID parameter not constructed correctly"); + }), + TestCase("succeeds for integer ID", () => { var it = Parameters.Id(88); Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.@string("88"), "ID parameter value incorrect"); + Expect.equal(it.Item2, Sql.@int(88), "ID parameter value incorrect"); }), - TestCase("Json succeeds", () => + TestCase("succeeds for unsigned integer ID", () => { - var it = Parameters.Json("@test", new { Something = "good" }); - Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); - Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); + var it = Parameters.Id((uint)889); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@int(889), "Unsigned int ID parameter not constructed correctly"); }), - TestList("AddField", new [] + TestCase("succeeds for long ID", () => { - TestCase("succeeds when a parameter is added", () => - { - var it = Parameters - .AddField("@field", Field.EQ("it", "242"), Enumerable.Empty>()) - .ToList(); - Expect.hasLength(it, 1, "There should have been a parameter added"); - Expect.equal(it[0].Item1, "@field", "Field parameter not constructed correctly"); - Expect.isTrue(it[0].Item2.IsParameter, "Field parameter value incorrect"); - }), - TestCase("succeeds when a parameter is not added", () => - { - var it = Parameters.AddField("@it", Field.EX("It"), Enumerable.Empty>()); - Expect.isEmpty(it, "There should not have been any parameters added"); - }), - TestCase("succeeds when two parameters are added", () => - { - var it = Parameters.AddField("@field", Field.BT("that", "eh", "zed"), - Enumerable.Empty>()).ToList(); - Expect.hasLength(it, 2, "There should have been 2 parameters added"); - Expect.equal(it[0].Item1, "@fieldmin", "Minimum field name not correct"); - Expect.isTrue(it[0].Item2.IsParameter, "Minimum field parameter value incorrect"); - Expect.equal(it[1].Item1, "@fieldmax", "Maximum field name not correct"); - Expect.isTrue(it[1].Item2.IsParameter, "Maximum field parameter value incorrect"); - }) + var it = Parameters.Id((long)123); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(123), "Long ID parameter not constructed correctly"); + }), + TestCase("succeeds for unsigned long ID", () => + { + var it = Parameters.Id((ulong)6464); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.int64(6464), "Unsigned long ID parameter not constructed correctly"); + }), + TestCase("succeeds for decimal ID", () => + { + var it = Parameters.Id((decimal)4.56); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@decimal((decimal)4.56), "Decimal ID parameter not constructed correctly"); + }), + TestCase("succeeds for single ID", () => + { + var it = Parameters.Id((float)5.67); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double((float)5.67), "Single ID parameter not constructed correctly"); + }), + TestCase("succeeds for double ID", () => + { + var it = Parameters.Id(6.78); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@double(6.78), "Double ID parameter not constructed correctly"); + }), + TestCase("succeeds for string ID", () => + { + var it = Parameters.Id("99"); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("99"), "ID parameter value incorrect"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + var it = Parameters.Id(new Uri("https://example.com")); + Expect.equal(it.Item1, "@id", "ID parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.@string("https://example.com/"), + "Non-numeric, non-string parameter value incorrect"); }) - }), - TestList("Query", new[] + ]), + TestCase("Json succeeds", () => { - TestList("WhereByField", new[] + var it = Parameters.Json("@test", new { Something = "good" }); + Expect.equal(it.Item1, "@test", "JSON parameter not constructed correctly"); + Expect.equal(it.Item2, Sql.jsonb("{\"Something\":\"good\"}"), "JSON parameter value incorrect"); + }), + TestList("AddFields", + [ + TestCase("succeeds when a parameter is added", () => { - TestCase("succeeds when a logical operator is passed", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.GT("theField", 0), "@test"), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds when an existence operator is passed", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL", - "WHERE clause not correct"); - }), - TestCase("succeeds when a between operator is passed with numeric values", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), - "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); - }), - TestCase("succeeds when a between operator is passed with non-numeric values", () => - { - Expect.equal(Postgres.Query.WhereByField(Field.BT("field0", "a", "b"), "@alpha"), - "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); - }) + var paramList = Parameters.AddFields([Field.EQ("it", "242")], []).ToList(); + Expect.hasLength(paramList, 1, "There should have been a parameter added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@field0", "Field parameter name not correct"); + Expect.equal(value, Sql.@string("242"), "Field parameter value not correct"); }), - TestCase("WhereById succeeds", () => + TestCase("succeeds when multiple independent parameters are added", () => { - Expect.equal(Postgres.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); + var paramList = Parameters.AddFields([Field.EQ("me", "you"), Field.GT("us", "them")], + [Parameters.Id(14)]).ToList(); + Expect.hasLength(paramList, 3, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@id", "First field parameter name not correct"); + Expect.equal(value, Sql.@int(14), "First field parameter value not correct"); + (name, value) = paramList[1]; + Expect.equal(name, "@field0", "Second field parameter name not correct"); + Expect.equal(value, Sql.@string("you"), "Second field parameter value not correct"); + (name, value) = paramList[2]; + Expect.equal(name, "@field1", "Third parameter name not correct"); + Expect.equal(value, Sql.@string("them"), "Third parameter value not correct"); }), - TestList("Definition", new[] + TestCase("succeeds when a parameter is not added", () => { - TestCase("EnsureTable succeeds", () => - { - Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), - $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)", - "CREATE TABLE statement not constructed correctly"); - }), - TestCase("EnsureDocumentIndex succeeds for full index", () => - { - Expect.equal(Postgres.Query.Definition.EnsureDocumentIndex("schema.tbl", DocumentIndex.Full), - "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)", - "CREATE INDEX statement not constructed correctly"); - }), - TestCase("EnsureDocumentIndex succeeds for JSONB Path Ops index", () => - { - Expect.equal( - Postgres.Query.Definition.EnsureDocumentIndex(PostgresDb.TableName, DocumentIndex.Optimized), - string.Format( - "CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)", - PostgresDb.TableName), - "CREATE INDEX statement not constructed correctly"); - }) + var paramList = Parameters.AddFields([Field.EX("tacos")], []).ToList(); + Expect.isEmpty(paramList, "There should not have been any parameters added"); }), - TestCase("Update succeeds", () => + TestCase("succeeds when two parameters are added for one field", () => { - Expect.equal(Postgres.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", - "UPDATE full statement not correct"); - }), - TestCase("WhereDataContains succeeds", () => - { - Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", - "WHERE clause not correct"); - }), - TestCase("WhereJsonPathMatches succeeds", () => - { - Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", - "WHERE clause not correct"); - }), - TestList("Count", new[] - { - TestCase("All succeeds", () => - { - Expect.equal(Postgres.Query.Count.All(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}", "Count query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByField(PostgresDb.TableName, Field.EQ("thatField", 0)), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field", - "JSON field text comparison count query not correct"); - }), - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria", - "JSON containment count query not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Count.ByJsonPath(PostgresDb.TableName), - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "JSON Path match count query not correct"); - }) - }), - TestList("Exists", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ById(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it", - "ID existence query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByField(PostgresDb.TableName, Field.LT("Test", 0)), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it", - "JSON field text comparison exists query not correct"); - }), - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it", - "JSON containment exists query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Exists.ByJsonPath(PostgresDb.TableName), - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it", - "JSON Path match existence query not correct"); - }) - }), - TestList("Find", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Find.ById(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", - "SELECT by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByField(PostgresDb.TableName, Field.GE("Golf", 0)), - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field", - "SELECT by JSON comparison query not correct"); - }), - TestCase("byContains succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria", - "SELECT by JSON containment query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Find.ByJsonPath(PostgresDb.TableName), - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "SELECT by JSON Path match query not correct"); - }) - }), - TestList("Patch", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, Field.LT("Snail", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field", - "UPDATE partial by ID statement not correct"); - }), - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByContains(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", - "UPDATE partial by JSON containment statement not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Patch.ByJsonPath(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", - "UPDATE partial by JSON Path statement not correct"); - }) - }), - TestList("RemoveFields", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Id' = @id", - "Remove field by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByField(PostgresDb.TableName, Field.LT("Fly", 0)), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data->>'Fly' < @field", - "Remove field by field query not correct"); - }), - TestCase("ByContains succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByContains(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @> @criteria", - "Remove field by contains query not correct"); - }), - TestCase("ByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.RemoveFields.ByJsonPath(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data - @name WHERE data @? @path::jsonpath", - "Remove field by JSON path query not correct"); - }) - }), - TestList("Delete", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ById(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id", - "DELETE by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByField(PostgresDb.TableName, Field.NEX("gone")), - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL", - "DELETE by JSON comparison query not correct"); - }), - TestCase("byContains succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria", - "DELETE by JSON containment query not correct"); - }), - TestCase("byJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Delete.ByJsonPath(PostgresDb.TableName), - $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - "DELETE by JSON Path match query not correct"); - }) + var paramList = + Parameters.AddFields([Field.BT("that", "eh", "zed").WithParameterName("@test")], []).ToList(); + Expect.hasLength(paramList, 2, "There should have been 2 parameters added"); + var (name, value) = paramList[0]; + Expect.equal(name, "@testmin", "Minimum field name not correct"); + Expect.equal(value, Sql.@string("eh"), "Minimum field value not correct"); + (name, value) = paramList[1]; + Expect.equal(name, "@testmax", "Maximum field name not correct"); + Expect.equal(value, Sql.@string("zed"), "Maximum field value not correct"); }) + ]), + TestList("FieldNames", + [ + TestCase("succeeds for one name", () => + { + var (name, value) = Parameters.FieldNames(["bob"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsString) + { + Expect.isTrue(false, "The parameter was not a String type"); + } + }), + TestCase("succeeds for multiple names", () => + { + var (name, value) = Parameters.FieldNames(["bob", "tom", "mike"]); + Expect.equal(name, "@name", "The parameter name was incorrect"); + if (!value.IsStringArray) + { + Expect.isTrue(false, "The parameter was not a StringArray type"); + } + }) + ]), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); }) - }); + ]); - private static readonly List TestDocuments = new() - { + /// + /// Unit tests for the Query module of the PostgreSQL library + /// + private static readonly Test QueryTests = TestList("Query", + [ + TestList("WhereByFields", + [ + TestCase("succeeds for a single field when a logical operator is passed", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.GT("theField", "0").WithParameterName("@test")]), + "data->>'theField' > @test", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when an existence operator is passed", () => + { + Expect.equal(Postgres.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), + "data->>'thatField' IS NULL", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when a between operator is passed with numeric values", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99).WithParameterName("@range")]), + "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); + }), + TestCase("succeeds for a single field when a between operator is passed with non-numeric values", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.BT("field0", "a", "b").WithParameterName("@alpha")]), + "data->>'field0' BETWEEN @alphamin AND @alphamax", "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with logical operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); + }), + TestCase("succeeds for any multiple fields with an existence operator", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.Any, + [Field.NEX("thatField"), Field.GE("thisField", 18)]), + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0", + "WHERE clause not correct"); + }), + TestCase("succeeds for all multiple fields with between operators", () => + { + Expect.equal( + Postgres.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); + }) + ]), + TestList("WhereById", + [ + TestCase("succeeds for numeric ID", () => + { + Expect.equal(Postgres.Query.WhereById(18), "(data->>'Id')::numeric = @id", "WHERE clause not correct"); + }), + TestCase("succeeds for string ID", () => + { + Expect.equal(Postgres.Query.WhereById("18"), "data->>'Id' = @id", "WHERE clause not correct"); + }), + TestCase("succeeds for non-numeric non-string ID", () => + { + Expect.equal(Postgres.Query.WhereById(new Uri("https://example.com")), "data->>'Id' = @id", + "WHERE clause not correct"); + }), + ]), + TestList("Definition", + [ + TestCase("EnsureTable succeeds", () => + { + Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)", + "CREATE TABLE statement not constructed correctly"); + }), + TestCase("EnsureDocumentIndex succeeds for full index", () => + { + Expect.equal(Postgres.Query.Definition.EnsureDocumentIndex("schema.tbl", DocumentIndex.Full), + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)", + "CREATE INDEX statement not constructed correctly"); + }), + TestCase("EnsureDocumentIndex succeeds for JSONB Path Ops index", () => + { + Expect.equal( + Postgres.Query.Definition.EnsureDocumentIndex(PostgresDb.TableName, DocumentIndex.Optimized), + string.Format( + "CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)", + PostgresDb.TableName), + "CREATE INDEX statement not constructed correctly"); + }) + ]), + TestCase("WhereDataContains succeeds", () => + { + Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", "WHERE clause not correct"); + }), + TestCase("WhereJsonPathMatches succeeds", () => + { + Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", + "WHERE clause not correct"); + }), + TestCase("Patch succeeds", () => + { + Expect.equal(Postgres.Query.Patch(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Postgres.Query.RemoveFields(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data - @name", "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Postgres.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Postgres.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE (data->>'That')::numeric > @field0", "By-Field query not correct"); + }), + TestCase("ByContains succeeds", () => + { + Expect.equal(Postgres.Query.ByContains("exam"), "exam WHERE data @> @criteria", + "By-Contains query not correct"); + }), + TestCase("ByPathMach succeeds", () => + { + Expect.equal(Postgres.Query.ByPathMatch("verify"), "verify WHERE data @? @path::jsonpath", + "By-JSON Path query not correct"); + }) + ]); + + private static readonly List 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 } - }; + ]; /// /// Add the test documents to the database @@ -310,961 +332,1290 @@ public static class PostgresCSharpTests } /// - /// Integration tests for the PostgreSQL library + /// Integration tests for the Configuration module of the PostgreSQL library /// - private static readonly Test Integration = TestList("Integration", new[] - { - TestList("Configuration", new[] + private static readonly Test ConfigurationTests = TestList("Configuration", + [ + TestCase("UseDataSource disposes existing source", () => { - TestCase("UseDataSource disposes existing source", () => - { - using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - var source = PostgresDb.MkDataSource(db1.ConnectionString); - Postgres.Configuration.UseDataSource(source); + using var db1 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + var source = PostgresDb.MkDataSource(db1.ConnectionString); + Postgres.Configuration.UseDataSource(source); - using var db2 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - Postgres.Configuration.UseDataSource(PostgresDb.MkDataSource(db2.ConnectionString)); + using var db2 = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + Postgres.Configuration.UseDataSource(PostgresDb.MkDataSource(db2.ConnectionString)); + try + { + _ = source.OpenConnection(); + Expect.isTrue(false, "Data source should have been disposed"); + } + catch (Exception) + { + // This is what should have happened + } + }), + TestCase("DataSource returns configured data source", () => + { + using var db = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); + var source = PostgresDb.MkDataSource(db.ConnectionString); + Postgres.Configuration.UseDataSource(source); + + Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), + "Data source should have been the same"); + }) + ]); + + /// + /// Integration tests for the Custom module of the PostgreSQL library + /// + private static readonly Test CustomTests = TestList("Custom", + [ + TestList("List", + [ + TestCase("succeeds when data is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List(Query.Find(PostgresDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); + }), + TestCase("succeeds when data is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List( + $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))], Results.FromData); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("Single", + [ + TestCase("succeeds when a row is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", + [Tuple.Create("@id", Sql.@string("one"))], Results.FromData); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when a row is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", + [Tuple.Create("@id", Sql.@string("eighty"))], Results.FromData); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("NonQuery", + [ + TestCase("succeeds when operating on data", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None); + + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 0, "There should be no documents remaining in the table"); + }), + TestCase("succeeds when no data matches where clause", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + [Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)"))]); + + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); + }) + ]), + TestCase("Scalar succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + + var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value")); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }) + ]); + + /// + /// Integration tests for the Definition module of the PostgreSQL library + /// + private static readonly Test DefinitionTests = TestList("Definition", + [ + TestCase("EnsureTable succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + + var exists = await TableExists(); + var alsoExists = await KeyExists(); + Expect.isFalse(exists, "The table should not exist already"); + Expect.isFalse(alsoExists, "The key index should not exist already"); + + await Definition.EnsureTable("ensured"); + exists = await TableExists(); + alsoExists = await KeyExists(); + Expect.isTrue(exists, "The table should now exist"); + Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + Task TableExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, + Results.ToExists); + + Task KeyExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None, + Results.ToExists); + }), + TestCase("EnsureDocumentIndex succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + + var exists = await IndexExists(); + Expect.isFalse(exists, "The index should not exist already"); + + await Definition.EnsureTable("ensured"); + await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); + exists = await IndexExists(); + Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", + Parameters.None, Results.ToExists); + }), + TestCase("EnsureFieldIndex succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + + var exists = await IndexExists(); + Expect.isFalse(exists, "The index should not exist already"); + + await Definition.EnsureTable("ensured"); + await Definition.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); + Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => Custom.Scalar( + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None, + Results.ToExists); + }) + ]); + + /// + /// Integration tests for the Document module of the PostgreSQL library + /// + private static readonly Test DocumentTests = TestList("Document", + [ + TestList("Insert", + [ + TestCase("succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await Count.All(PostgresDb.TableName); + Expect.equal(after, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = PostgresDb.BuildDb(); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); try { - _ = source.OpenConnection(); - Expect.isTrue(false, "Data source should have been disposed"); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); } catch (Exception) { // This is what should have happened } }), - TestCase("DataSource returns configured data source", () => + TestCase("succeeds when adding a numeric auto ID", async () => { - using var db = ThrowawayDatabase.Create(PostgresDb.ConnStr.Value); - var source = PostgresDb.MkDataSource(db.ConnectionString); - Postgres.Configuration.UseDataSource(source); - - Expect.isTrue(ReferenceEquals(source, Postgres.Configuration.DataSource()), - "Data source should have been the same"); - }) - }), - TestList("Custom", new[] - { - TestList("List", new[] - { - TestCase("succeeds when data is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Custom.List(Query.SelectFromTable(PostgresDb.TableName), Parameters.None, - Results.FromData); - Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); - }), - TestCase("succeeds when data is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Custom.List( - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }, - Results.FromData); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("Single", new[] - { - TestCase("succeeds when a row is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("one")) }, Results.FromData); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when a row is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Custom.Single($"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Tuple.Create("@id", Sql.@string("eighty")) }, Results.FromData); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("NonQuery", new[] - { - TestCase("succeeds when operating on data", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None); - - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 0, "There should be no documents remaining in the table"); - }), - TestCase("succeeds when no data matches where clause", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", - new[] { Tuple.Create("@path", Sql.@string("$.NumValue ? (@ > 100)")) }); - - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should be 5 documents remaining in the table"); - }) - }), - TestCase("Scalar succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - - var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value")); - Expect.equal(nbr, 5, "The query should have returned the number 5"); - }) - }), - TestList("Definition", new[] - { - TestCase("EnsureTable succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - var tableExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it", Parameters.None, - Results.ToExists); - var keyExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it", Parameters.None, - Results.ToExists); - - var exists = await tableExists(); - var alsoExists = await keyExists(); - Expect.isFalse(exists, "The table should not exist already"); - Expect.isFalse(alsoExists, "The key index should not exist already"); - - await Definition.EnsureTable("ensured"); - exists = await tableExists(); - alsoExists = await keyExists(); - Expect.isTrue(exists, "The table should now exist"); - Expect.isTrue(alsoExists, "The key index should now exist"); - }), - TestCase("EnsureDocumentIndex succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - var indexExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it", - Parameters.None, Results.ToExists); - - var exists = await indexExists(); - Expect.isFalse(exists, "The index should not exist already"); - - await Definition.EnsureTable("ensured"); - await Definition.EnsureDocumentIndex("ensured", DocumentIndex.Optimized); - exists = await indexExists(); - Expect.isTrue(exists, "The index should now exist"); - }), - TestCase("EnsureFieldIndex succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - var indexExists = () => Custom.Scalar( - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it", Parameters.None, - Results.ToExists); - - var exists = await indexExists(); - Expect.isFalse(exists, "The index should not exist already"); - - await Definition.EnsureTable("ensured"); - await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); - Expect.isTrue(exists, "The index should now exist"); - }) - }), - TestList("Document", new[] - { - TestList("Insert", new[] - { - TestCase("succeeds", async () => + try { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); await using var db = PostgresDb.BuildDb(); var before = await Count.All(PostgresDb.TableName); Expect.equal(before, 0, "There should be no documents in the table"); - - await Document.Insert(PostgresDb.TableName, - new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); - var after = await Count.All(PostgresDb.TableName); - Expect.equal(after, 1, "There should have been one document inserted"); - }), - TestCase("fails for duplicate key", async () => + + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally { - await using var db = PostgresDb.BuildDb(); - await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); - try - { - await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); - Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); - } - catch (Exception) - { - // This is what should have happened - } - }) + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } }), - TestList("Save", new[] + TestCase("succeeds when adding a GUID auto ID", async () => { - TestCase("succeeds when a document is inserted", async () => + try { + Configuration.UseAutoIdStrategy(AutoId.Guid); await using var db = PostgresDb.BuildDb(); var before = await Count.All(PostgresDb.TableName); Expect.equal(before, 0, "There should be no documents in the table"); - - await Document.Save(PostgresDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - var after = await Count.All(PostgresDb.TableName); - Expect.equal(after, 1, "There should have been one document inserted"); - }), - TestCase("succeeds when a document is updated", async () => + + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); await using var db = PostgresDb.BuildDb(); - await Document.Insert(PostgresDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); - var before = await Find.ById(PostgresDb.TableName, "test"); - Expect.isNotNull(before, "There should have been a document returned"); - Expect.equal(before.Id, "test", "The document is not correct"); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); - before.Sub = new() { Foo = "c", Bar = "d" }; - await Document.Save(PostgresDb.TableName, before); - var after = await Find.ById(PostgresDb.TableName, "test"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "test", "The document is not correct"); - Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); - }) + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) - }), - TestList("Count", new[] + ]), + TestList("Save", + [ + TestCase("succeeds when a document is inserted", async () => + { + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Save(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await Count.All(PostgresDb.TableName); + Expect.equal(after, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await Document.Insert(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await Find.ById(PostgresDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before.Id, "test", "The document is not correct"); + + before.Sub = new() { Foo = "c", Bar = "d" }; + await Document.Save(PostgresDb.TableName, before); + var after = await Find.ById(PostgresDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "test", "The document is not correct"); + Expect.equal(after.Sub!.Foo, "c", "The updated document is not correct"); + }) + ]) + ]); + + /// + /// Integration tests for the Count module of the PostgreSQL library + /// + private static readonly Test CountTests = TestList("Count", + [ + TestCase("All succeeds", async () => { - TestCase("All succeeds", async () => + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.All(PostgresDb.TableName); + Expect.equal(theCount, 5, "There should have been 5 matching documents"); + }), + TestList("ByFields", + [ + TestCase("succeeds for numeric range", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var theCount = await Count.All(PostgresDb.TableName); - Expect.equal(theCount, 5, "There should have been 5 matching documents"); - }), - TestCase("ByField succeeds for numeric range", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("NumValue", 10, 20)); + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.BT("NumValue", 10, 20)]); Expect.equal(theCount, 3, "There should have been 3 matching documents"); }), - TestCase("ByField succeeds for non-numeric range", async () => + TestCase("succeeds for non-numeric range", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - var theCount = await Count.ByField(PostgresDb.TableName, Field.BT("Value", "aardvark", "apple")); + var theCount = await Count.ByFields(PostgresDb.TableName, FieldMatch.All, + [Field.BT("Value", "aardvark", "apple")]); Expect.equal(theCount, 1, "There should have been 1 matching document"); - }), - TestCase("ByContains succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByContains(PostgresDb.TableName, new { Value = "purple" }); - Expect.equal(theCount, 2, "There should have been 2 matching documents"); - }), - TestCase("ByJsonPath succeeds", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); - Expect.equal(theCount, 3, "There should have been 3 matching documents"); }) - }), - TestList("Exists", new[] + ]), + TestCase("ByContains succeeds", async () => { - TestList("ById", new[] - { - TestCase("succeeds when a document exists", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); - var exists = await Exists.ById(PostgresDb.TableName, "three"); - Expect.isTrue(exists, "There should have been an existing document"); - }), - TestCase("succeeds when a document does not exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(PostgresDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByField(PostgresDb.TableName, Field.NEX("Sub")); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when documents do not exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByField(PostgresDb.TableName, Field.EQ("NumValue", "six")); - Expect.isFalse(exists, "There should not have been existing documents"); - }) - }), - TestList("ByContains", new[] - { - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByContains(PostgresDb.TableName, new { NumValue = 10 }); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - }), - TestList("ByJsonPath", new[] { - TestCase("succeeds when documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - }) + var theCount = await Count.ByContains(PostgresDb.TableName, new { Value = "purple" }); + Expect.equal(theCount, 2, "There should have been 2 matching documents"); }), - TestList("Find", new[] + TestCase("ByJsonPath succeeds", async () => { - TestList("All", new[] - { - TestCase("succeeds when there is data", async () => - { - await using var db = PostgresDb.BuildDb(); + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "one", Bar = "two" }); - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "three", Bar = "four" }); - await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "five", Bar = "six" }); - - var results = await Find.All(PostgresDb.TableName); - Expect.equal(results.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when there is no data", async () => - { - await using var db = PostgresDb.BuildDb(); - var results = await Find.All(PostgresDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) - }), - TestList("ById", new[] - { - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(PostgresDb.TableName, "three hundred eighty-seven"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByField(PostgresDb.TableName, Field.EQ("Value", "another")); - Expect.equal(docs.Count, 1, "There should have been one document returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByField(PostgresDb.TableName, Field.EQ("Value", "mauve")); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("ByContains", new[] - { - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByContains(PostgresDb.TableName, - new { Sub = new { Foo = "green" } }); - Expect.equal(docs.Count, 2, "There should have been two documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByContains(PostgresDb.TableName, new { Value = "mauve" }); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("ByJsonPath", new[] - { - TestCase("succeeds when documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 15)"); - Expect.equal(docs.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("FirstByField", new[] - { - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "another")); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "purple")); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "five", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(PostgresDb.TableName, Field.EQ("Value", "absent")); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("FirstByContains", new[] - { - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "another" }); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, - new { Sub = new { Foo = "green" } }); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "absent" }); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("FirstByJsonPath", new[] - { - TestCase("succeeds when a document is found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, - "$.Value ? (@ == \"FIRST!\")"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, - "$.Sub.Foo ? (@ == \"green\")"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }) - }), - TestList("Update", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Update.ById(PostgresDb.TableName, "one", - new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); - Expect.equal(after.Value, "", "The updated document is not correct (Value)"); - Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)"); - Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); - Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)"); - Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ById(PostgresDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) - }), - TestList("ByFunc", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); - Expect.equal(after.Value, "le un", "The updated document is not correct (Value)"); - Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)"); - Expect.isNull(after.Sub, "The updated document should not have had a sub-document"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - }) - }), - TestList("Patch", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ById(PostgresDb.TableName, "one", new { NumValue = 44 }); - var after = await Find.ById(PostgresDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after.NumValue, 44, "The updated document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await Count.ByField(PostgresDb.TableName, Field.EQ("NumValue", "77")); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByField(PostgresDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); - }) - }), - TestList("ByContains", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 }); - var after = await Count.ByContains(PostgresDb.TableName, new { NumValue = 77 }); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); - }) - }), - TestList("ByJsonPath", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 }); - var after = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)"); - Expect.equal(after, 2, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = PostgresDb.BuildDb(); - - var before = await Count.All(PostgresDb.TableName); - Expect.equal(before, 0, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); - }) - }) - }), - TestList("RemoveFields", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ById(PostgresDb.TableName, "two", new[] { "Value" }); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByField(PostgresDb.TableName, Field.EQ("NumValue", "17"), new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByField(PostgresDb.TableName, Field.NE("Abracadabra", "apple"), - new[] { "Value" }); - }) - }), - TestList("ByContains", new[] - { - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, - new[] { "Value" }); - }) - }), - TestList("ByJsonPath", new[] - { - TestCase("succeeds when multiple fields are removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", - new[] { "Sub", "Value" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a single field is removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Sub" }); - var updated = await Find.ById(PostgresDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.notEqual(updated.Value, "", "The string value should not have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = PostgresDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", - new[] { "Value" }); - }) - }) - }), - TestList("Delete", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when a document is deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(PostgresDb.TableName, "four"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 4, "There should have been 4 documents remaining"); - }), - TestCase("succeeds when a document is not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(PostgresDb.TableName, "thirty"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByField(PostgresDb.TableName, Field.EQ("Value", "purple")); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByField(PostgresDb.TableName, Field.EQ("Value", "crimson")); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - }), - TestList("ByContains", new[] - { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByContains(PostgresDb.TableName, new { Value = "purple" }); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByContains(PostgresDb.TableName, new { Value = "crimson" }); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - }), - TestList("ByJsonPath", new[] - { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 3, "There should have been 3 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = PostgresDb.BuildDb(); - await LoadDocs(); - - await Delete.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)"); - var remaining = await Count.All(PostgresDb.TableName); - Expect.equal(remaining, 5, "There should have been 5 documents remaining"); - }) - }) + var theCount = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); + Expect.equal(theCount, 3, "There should have been 3 matching documents"); }) - }); - + ]); + + /// + /// Integration tests for the Exists module of the PostgreSQL library + /// + private static readonly Test ExistsTests = TestList("Exists", + [ + TestList("ById", + [ + TestCase("succeeds when a document exists", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(PostgresDb.TableName, "three"); + Expect.isTrue(exists, "There should have been an existing document"); + }), + TestCase("succeeds when a document does not exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ById(PostgresDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NEX("Sub")]); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when documents do not exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "six")]); + Expect.isFalse(exists, "There should not have been existing documents"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByContains(PostgresDb.TableName, new { NumValue = 10 }); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when no matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByContains(PostgresDb.TableName, new { Nothing = "none" }); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when no matching documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + ]) + ]); + + /// + /// Integration tests for the Find module of the PostgreSQL library + /// + private static readonly Test FindTests = TestList("Find", + [ + TestList("All", + [ + TestCase("succeeds when there is data", async () => + { + await using var db = PostgresDb.BuildDb(); + + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "one", Bar = "two" }); + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "three", Bar = "four" }); + await Document.Insert(PostgresDb.TableName, new SubDocument { Foo = "five", Bar = "six" }); + + var results = await Find.All(PostgresDb.TableName); + Expect.hasLength(results, 3, "There should have been 3 documents returned"); + }), + TestCase("succeeds when there is no data", async () => + { + await using var db = PostgresDb.BuildDb(); + var results = await Find.All(PostgresDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + ]), + TestList("AllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = + await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = + await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var results = await Find.AllOrdered(PostgresDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("ById", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(PostgresDb.TableName, "three hundred eighty-seven"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.hasLength(docs, 1, "There should have been one document returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.hasLength(docs, 2, "There should have been two document returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContains(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContains(PostgresDb.TableName, new { Value = "mauve" }); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByContainsOrdered", + [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|four", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Sub.Bar DESC")]); + Expect.hasLength(docs, 2, "There should have been two documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|two", + "Documents not ordered correctly"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 15)"); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByJsonPathOrdered", + [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "one|three|two", + "Documents not ordered correctly"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByJsonPathOrdered(PostgresDb.TableName, "$.NumValue ? (@ < 15)", + [Field.Named("n:NumValue DESC")]); + Expect.hasLength(docs, 3, "There should have been 3 documents returned"); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "two|three|one", + "Documents not ordered correctly"); + }) + ]), + TestList("FirstByFields", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["five", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("five", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(PostgresDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "purple")], [Field.Named("Id DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("FirstByContains", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "another" }); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContains(PostgresDb.TableName, new { Value = "absent" }); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByContainsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByContainsOrdered(PostgresDb.TableName, + new { Sub = new { Foo = "green" } }, [Field.Named("Value DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]), + TestList("FirstByJsonPath", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, + "$.Value ? (@ == \"FIRST!\")"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByJsonPathOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByJsonPathOrdered(PostgresDb.TableName, + "$.Sub.Foo ? (@ == \"green\")", [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc.Id, "An incorrect document was returned"); + }) + ]) + ]); + + /// + /// Integration tests for the Update module of the PostgreSQL library + /// + private static readonly Test UpdateTests = TestList("Update", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.ById(PostgresDb.TableName, "one", + new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); + Expect.equal(after.Value, "", "The updated document is not correct (Value)"); + Expect.equal(after.NumValue, 0, "The updated document is not correct (NumValue)"); + Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); + Expect.equal(after.Sub!.Foo, "blue", "The updated document is not correct (Sub.Foo)"); + Expect.equal(after.Sub.Bar, "red", "The updated document is not correct (Sub.Bar)"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ById(PostgresDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + ]), + TestList("ByFunc", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.Id, "one", "The updated document is not correct (ID)"); + Expect.equal(after.Value, "le un", "The updated document is not correct (Value)"); + Expect.equal(after.NumValue, 1, "The updated document is not correct (NumValue)"); + Expect.isNull(after.Sub, "The updated document should not have had a sub-document"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + ]) + ]); + + /// + /// Integration tests for the Patch module of the PostgreSQL library + /// + private static readonly Test PatchTests = TestList("Patch", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ById(PostgresDb.TableName, "one", new { NumValue = 44 }); + var after = await Find.ById(PostgresDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after.NumValue, 44, "The updated document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "77")]); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 }); + var after = await Count.ByContains(PostgresDb.TableName, new { NumValue = 77 }); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 }); + var after = await Count.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)"); + Expect.equal(after, 2, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + }) + ]) + ]); + + /// + /// Integration tests for the RemoveFields module of the PostgreSQL library + /// + private static readonly Test RemoveFieldsTests = TestList("RemoveFields", + [ + TestList("ById", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(PostgresDb.TableName, "two", ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(PostgresDb.TableName, "two", ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ById(PostgresDb.TableName, "two", ["AFieldThatIsNotThere"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ById(PostgresDb.TableName, "two", ["Value"]); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", "17")], + ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByContains(PostgresDb.TableName, new { NumValue = 17 }, ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByContains(PostgresDb.TableName, new { Abracadabra = "apple" }, ["Value"]); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when multiple fields are removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub", "Value"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a single field is removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Sub"]); + var updated = await Find.ById(PostgresDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.notEqual(updated.Value, "", "The string value should not have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ == 17)", ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = PostgresDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByJsonPath(PostgresDb.TableName, "$.Abracadabra ? (@ == \"apple\")", ["Value"]); + }) + ]) + ]); + + /// + /// Integration tests for the Delete module of the PostgreSQL library + /// + private static readonly Test DeleteTests = TestList("Delete", + [ + TestList("ById", + [ + TestCase("succeeds when a document is deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(PostgresDb.TableName, "four"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 4, "There should have been 4 documents remaining"); + }), + TestCase("succeeds when a document is not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(PostgresDb.TableName, "thirty"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(PostgresDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByContains", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByContains(PostgresDb.TableName, new { Value = "purple" }); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByContains(PostgresDb.TableName, new { Value = "crimson" }); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByJsonPath", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 3, "There should have been 3 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Delete.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)"); + var remaining = await Count.All(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + ]) + ]); + /// /// All Postgres C# tests /// [Tests] - public static readonly Test All = TestList("Postgres.C#", new[] { Unit, TestSequenced(Integration) }); + public static readonly Test All = TestList("Postgres.C#", + [ + TestList("Unit", [ParametersTests, QueryTests]), + TestSequenced(TestList("Integration", + [ + ConfigurationTests, + CustomTests, + DefinitionTests, + DocumentTests, + CountTests, + ExistsTests, + FindTests, + UpdateTests, + PatchTests, + RemoveFieldsTests, + DeleteTests + ])) + ]); } diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs index 301e9a6..4d84e2c 100644 --- a/src/Tests.CSharp/PostgresDb.cs +++ b/src/Tests.CSharp/PostgresDb.cs @@ -1,3 +1,4 @@ +using BitBadger.Documents.Postgres; using Npgsql; using Npgsql.FSharp; using ThrowawayDb.Postgres; @@ -131,7 +132,7 @@ public static class PostgresDb var sqlProps = Sql.connect(database.ConnectionString); Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps)); - Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps)); + Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName, Dialect.PostgreSQL), sqlProps)); Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString)); diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 3501695..7290183 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -1,6 +1,5 @@ using Expecto.CSharp; using Expecto; -using Microsoft.Data.Sqlite; using BitBadger.Documents.Sqlite; namespace BitBadger.Documents.Tests.CSharp; @@ -18,10 +17,10 @@ public static class SqliteCSharpExtensionTests /// Integration tests for the SQLite extension methods /// [Tests] - public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[] - { - TestList("CustomSingle", new[] - { + public static readonly Test Integration = TestList("Sqlite.C#.Extensions", + [ + TestList("CustomSingle", + [ TestCase("succeeds when a row is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -29,7 +28,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("one") }, Results.FromData); + [Parameters.Id("one")], Results.FromData); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "one", "The incorrect document was returned"); }), @@ -40,19 +39,19 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var doc = await conn.CustomSingle($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("eighty") }, Results.FromData); + [Parameters.Id("eighty")], Results.FromData); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("CustomList", new[] - { + ]), + TestList("CustomList", + [ TestCase("succeeds when data is found", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.CustomList(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + var docs = await conn.CustomList(Query.Find(SqliteDb.TableName), Parameters.None, Results.FromData); Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), @@ -63,13 +62,13 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var docs = await conn.CustomList( - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }, Results.FromData); + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", [new("@value", 100)], + Results.FromData); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("CustomNonQuery", new[] - { + ]), + TestList("CustomNonQuery", + [ TestCase("succeeds when operating on data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -88,12 +87,12 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }); + [new("@value", 100)]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); }) - }), + ]), TestCase("CustomScalar succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -107,41 +106,44 @@ public static class SqliteCSharpExtensionTests await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); - Func> itExists = async name => - await conn.CustomScalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, Results.ToExists); - - var exists = await itExists("ensured"); - var alsoExists = await itExists("idx_ensured_key"); + var exists = await ItExists("ensured"); + var alsoExists = await ItExists("idx_ensured_key"); Expect.isFalse(exists, "The table should not exist already"); Expect.isFalse(alsoExists, "The key index should not exist already"); await conn.EnsureTable("ensured"); - exists = await itExists("ensured"); - alsoExists = await itExists("idx_ensured_key"); + exists = await ItExists("ensured"); + alsoExists = await ItExists("idx_ensured_key"); Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + Task ItExists(string name) => + conn.CustomScalar($"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + [new("@name", name)], Results.ToExists); }), TestCase("EnsureFieldIndex succeeds", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); - var indexExists = () => conn.CustomScalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", - Parameters.None, Results.ToExists); - var exists = await indexExists(); + var exists = await IndexExists(); Expect.isFalse(exists, "The index should not exist already"); await conn.EnsureTable("ensured"); - await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); + await conn.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); Expect.isTrue(exists, "The index should now exist"); + return; + + Task IndexExists() => + conn.CustomScalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); }), - TestList("Insert", new[] - { + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -168,9 +170,9 @@ public static class SqliteCSharpExtensionTests // This is what is supposed to happen } }) - }), - TestList("Save", new[] - { + ]), + TestList("Save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -203,7 +205,7 @@ public static class SqliteCSharpExtensionTests Expect.equal(after!.Id, "test", "The updated document is not correct"); Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); }) - }), + ]), TestCase("CountAll succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -219,11 +221,11 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var theCount = await conn.CountByField(SqliteDb.TableName, Field.EQ("Value", "purple")); + var theCount = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")]); Expect.equal(theCount, 2L, "There should have been 2 matching documents"); }), - TestList("ExistsById", new[] - { + TestList("ExistsById", + [ TestCase("succeeds when a document exists", async () => { await using var db = await SqliteDb.BuildDb(); @@ -242,16 +244,16 @@ public static class SqliteCSharpExtensionTests var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("ExistsByField", new[] - { + ]), + TestList("ExistsByFields", + [ TestCase("succeeds when documents exist", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var exists = await conn.ExistsByField(SqliteDb.TableName, Field.GE("NumValue", 10)); + var exists = await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); Expect.isTrue(exists, "There should have been existing documents"); }), TestCase("succeeds when no matching documents exist", async () => @@ -260,12 +262,13 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var exists = await conn.ExistsByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); + var exists = + await conn.ExistsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]); Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("FindAll", new[] - { + ]), + TestList("FindAll", + [ TestCase("succeeds when there is data", async () => { await using var db = await SqliteDb.BuildDb(); @@ -285,9 +288,46 @@ public static class SqliteCSharpExtensionTests var results = await conn.FindAll(SqliteDb.TableName); Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("FindById", new[] - { + ]), + TestList("FindAllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering numerically descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = + await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); + }), + TestCase("succeeds when ordering alphabetically", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var results = await conn.FindAllOrdered(SqliteDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); + }) + ]), + TestList("FindById", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); @@ -307,16 +347,17 @@ public static class SqliteCSharpExtensionTests var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("FindByField", new[] - { + ]), + TestList("FindByFields", + [ TestCase("succeeds when documents are found", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.FindByField(SqliteDb.TableName, Field.GT("NumValue", 15)); + var docs = await conn.FindByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)]); Expect.equal(docs.Count, 2, "There should have been two documents returned"); }), TestCase("succeeds when documents are not found", async () => @@ -325,19 +366,46 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var docs = await conn.FindByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); + var docs = await conn.FindByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); Expect.isEmpty(docs, "There should have been no documents returned"); }) - }), - TestList("FindFirstByField", new[] - { + ]), + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id DESC")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "There should have been two documents returned"); + }) + ]), + TestList("FindFirstByFields", + [ TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Value", "another")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned"); }), @@ -347,9 +415,10 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Sub.Foo", "green")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")]); Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); + Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned"); }), TestCase("succeeds when a document is not found", async () => { @@ -357,12 +426,38 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var doc = await conn.FindFirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); + var doc = await conn.FindFirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); Expect.isNull(doc, "There should not have been a document returned"); }) - }), - TestList("UpdateById", new[] - { + ]), + TestList("FindFirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc!.Id, "An incorrect document was returned"); + }) + ]), + TestList("UpdateById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -389,9 +484,9 @@ public static class SqliteCSharpExtensionTests await conn.UpdateById(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) - }), - TestList("UpdateByFunc", new[] - { + ]), + TestList("UpdateByFunc", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -418,9 +513,9 @@ public static class SqliteCSharpExtensionTests await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - TestList("PatchById", new[] - { + ]), + TestList("PatchById", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); @@ -443,17 +538,18 @@ public static class SqliteCSharpExtensionTests // This not raising an exception is the test await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" }); }) - }), - TestList("PatchByField", new[] - { + ]), + TestList("PatchByFields", + [ TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await conn.CountByField(SqliteDb.TableName, Field.EQ("NumValue", 77)); + await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await conn.CountByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), TestCase("succeeds when no document is updated", async () => @@ -464,18 +560,19 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.PatchByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); + await conn.PatchByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); }) - }), - TestList("RemoveFieldsById", new[] - { + ]), + TestList("RemoveFieldsById", + [ TestCase("succeeds when fields are removed", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Sub", "Value" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Sub", "Value"]); var updated = await Find.ById(SqliteDb.TableName, "two"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.equal(updated.Value, "", "The string value should have been removed"); @@ -488,7 +585,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["AFieldThatIsNotThere"]); }), TestCase("succeeds when no document is matched", async () => { @@ -496,18 +593,19 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); // This not raising an exception is the test - await conn.RemoveFieldsById(SqliteDb.TableName, "two", new[] { "Value" }); + await conn.RemoveFieldsById(SqliteDb.TableName, "two", ["Value"]); }) - }), - TestList("RemoveFieldsByField", new[] - { + ]), + TestList("RemoveFieldsByFields", + [ TestCase("succeeds when a field is removed", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Sub" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Sub"]); var updated = await Find.ById(SqliteDb.TableName, "four"); Expect.isNotNull(updated, "The updated document should have been retrieved"); Expect.isNull(updated.Sub, "The sub-document should have been removed"); @@ -519,7 +617,8 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); // This not raising an exception is the test - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Nothing" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Nothing"]); }), TestCase("succeeds when no document is matched", async () => { @@ -527,11 +626,12 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); // This not raising an exception is the test - await conn.RemoveFieldsByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); + await conn.RemoveFieldsByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); }) - }), - TestList("DeleteById", new[] - { + ]), + TestList("DeleteById", + [ TestCase("succeeds when a document is deleted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -552,16 +652,16 @@ public static class SqliteCSharpExtensionTests var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), - TestList("DeleteByField", new[] - { + ]), + TestList("DeleteByFields", + [ TestCase("succeeds when documents are deleted", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.DeleteByField(SqliteDb.TableName, Field.NE("Value", "purple")); + await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); }), @@ -571,11 +671,11 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.DeleteByField(SqliteDb.TableName, Field.EQ("Value", "crimson")); + await conn.DeleteByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "crimson")]); var remaining = await conn.CountAll(SqliteDb.TableName); Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), + ]), TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + ]); } diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 6f38664..2ea73ca 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -1,7 +1,5 @@ -using System.Text.Json; -using Expecto.CSharp; +using Expecto.CSharp; using Expecto; -using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; using BitBadger.Documents.Sqlite; @@ -15,176 +13,133 @@ using static Runner; public static class SqliteCSharpTests { /// - /// Unit tests for the SQLite library + /// Unit tests for the Query module of the SQLite library /// - private static readonly Test Unit = TestList("Unit", new[] - { - TestList("Query", new[] - { - TestList("WhereByField", new[] + private static readonly Test QueryTests = TestList("Query", + [ + TestList("WhereByFields", + [ + TestCase("succeeds for a single field when a logical operator is passed", () => { - TestCase("succeeds when a logical operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.GT("theField", 0), "@test"), - "data->>'theField' > @test", "WHERE clause not correct"); - }), - TestCase("succeeds when an existence operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.NEX("thatField"), ""), "data->>'thatField' IS NULL", - "WHERE clause not correct"); - }), - TestCase("succeeds when the between operator is passed", () => - { - Expect.equal(Sqlite.Query.WhereByField(Field.BT("aField", 50, 99), "@range"), - "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); - }) + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.GT("theField", 0).WithParameterName("@test")]), + "data->>'theField' > @test", "WHERE clause not correct"); }), - TestCase("WhereById succeeds", () => + TestCase("succeeds for a single field when an existence operator is passed", () => { - Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); + Expect.equal(Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField")]), + "data->>'thatField' IS NULL", "WHERE clause not correct"); }), - TestCase("Definition.EnsureTable succeeds", () => + TestCase("succeeds for a single field when a between operator is passed", () => { - Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), - "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99).WithParameterName("@range")]), + "data->>'aField' BETWEEN @rangemin AND @rangemax", "WHERE clause not correct"); }), - TestCase("Update succeeds", () => + TestCase("succeeds for all multiple fields with logical operators", () => { - Expect.equal(Sqlite.Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data->>'Id' = @id", - "UPDATE full statement not correct"); + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, [Field.EQ("theFirst", "1"), Field.EQ("numberTwo", "2")]), + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1", "WHERE clause not correct"); }), - TestList("Count", new[] + TestCase("succeeds for any multiple fields with an existence operator", () => { - TestCase("All succeeds", () => - { - Expect.equal(Sqlite.Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", - "Count query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Count.ByField("tbl", Field.EQ("thatField", 0)), - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field", - "JSON field text comparison count query not correct"); - }) + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.Any, [Field.NEX("thatField"), Field.GE("thisField", 18)]), + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0", "WHERE clause not correct"); }), - TestList("Exists", new[] + TestCase("succeeds for all multiple fields with between operators", () => { - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Exists.ById("tbl"), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Id' = @id) AS it", - "ID existence query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Exists.ByField("tbl", Field.LT("Test", 0)), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it", - "JSON field text comparison exists query not correct"); - }) - }), - TestList("Find", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data->>'Id' = @id", - "SELECT by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Find.ByField("tbl", Field.GE("Golf", 0)), - "SELECT data FROM tbl WHERE data->>'Golf' >= @field", - "SELECT by JSON comparison query not correct"); - }) - }), - TestList("Patch", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Patch.ById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Patch.ByField("tbl", Field.NE("Part", 0)), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field", - "UPDATE partial by JSON comparison query not correct"); - }) - }), - TestList("RemoveFields", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.RemoveFields.ById("tbl", new[] { new SqliteParameter("@name", "one") }), - "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id", - "Remove field by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.RemoveFields.ByField("tbl", Field.LT("Fly", 0), - new[] { new SqliteParameter("@name0", "one"), new SqliteParameter("@name1", "two") }), - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' < @field", - "Remove field by field query not correct"); - }) - }), - TestList("Delete", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Sqlite.Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data->>'Id' = @id", - "DELETE by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Sqlite.Query.Delete.ByField("tbl", Field.NEX("gone")), - "DELETE FROM tbl WHERE data->>'gone' IS NULL", "DELETE by JSON comparison query not correct"); - }) + Expect.equal( + Sqlite.Query.WhereByFields(FieldMatch.All, + [Field.BT("aField", 50, 99), Field.BT("anotherField", "a", "b")]), + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max", + "WHERE clause not correct"); }) + ]), + TestCase("WhereById succeeds", () => + { + Expect.equal(Sqlite.Query.WhereById("@id"), "data->>'Id' = @id", "WHERE clause not correct"); }), - TestList("Parameters", new[] + TestCase("Patch succeeds", () => { - TestCase("Id succeeds", () => - { - var theParam = Parameters.Id(7); - Expect.equal(theParam.ParameterName, "@id", "The parameter name is incorrect"); - Expect.equal(theParam.Value, "7", "The parameter value is incorrect"); - }), - TestCase("Json succeeds", () => - { - var theParam = Parameters.Json("@test", new { Nice = "job" }); - Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect"); - Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect"); - }), - TestCase("AddField succeeds when adding a parameter", () => - { - var paramList = Parameters.AddField("@field", Field.EQ("it", 99), Enumerable.Empty()) - .ToList(); - Expect.hasLength(paramList, 1, "There should have been a parameter added"); - var theParam = paramList[0]; - Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); - Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); - }), - TestCase("AddField succeeds when not adding a parameter", () => - { - var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), Enumerable.Empty()); - Expect.isEmpty(paramSeq, "There should not have been any parameters added"); - }), - TestCase("None succeeds", () => - { - Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); - }) + Expect.equal(Sqlite.Query.Patch(SqliteDb.TableName), + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))", "Patch query not correct"); + }), + TestCase("RemoveFields succeeds", () => + { + Expect.equal(Sqlite.Query.RemoveFields(SqliteDb.TableName, [new("@a", "a"), new("@b", "b")]), + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)", + "Field removal query not correct"); + }), + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.ById("test", "14"), "test WHERE data->>'Id' = @id", "By-ID query not correct"); + }), + TestCase("ByFields succeeds", () => + { + Expect.equal(Sqlite.Query.ByFields("unit", FieldMatch.Any, [Field.GT("That", 14)]), + "unit WHERE data->>'That' > @field0", "By-Field query not correct"); + }), + TestCase("Definition.EnsureTable succeeds", () => + { + Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); }) - // Results are exhaustively executed in the context of other tests - }); + ]); - private static readonly List TestDocuments = new() - { + /// + /// Unit tests for the Parameters module of the SQLite library + /// + private static readonly Test ParametersTests = TestList("Parameters", + [ + TestCase("Id succeeds", () => + { + var theParam = Parameters.Id(7); + Expect.equal(theParam.ParameterName, "@id", "The parameter name is incorrect"); + Expect.equal(theParam.Value, "7", "The parameter value is incorrect"); + }), + TestCase("Json succeeds", () => + { + var theParam = Parameters.Json("@test", new { Nice = "job" }); + Expect.equal(theParam.ParameterName, "@test", "The parameter name is incorrect"); + Expect.equal(theParam.Value, "{\"Nice\":\"job\"}", "The parameter value is incorrect"); + }), +#pragma warning disable CS0618 + TestCase("AddField succeeds when adding a parameter", () => + { + var paramList = Parameters.AddField("@field", Field.EQ("it", 99), []).ToList(); + Expect.hasLength(paramList, 1, "There should have been a parameter added"); + var theParam = paramList[0]; + Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); + Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); + }), + TestCase("AddField succeeds when not adding a parameter", () => + { + var paramSeq = Parameters.AddField("@it", Field.EX("Coffee"), []); + Expect.isEmpty(paramSeq, "There should not have been any parameters added"); + }), +#pragma warning restore CS0618 + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); + }) + ]); + + // Results are exhaustively executed in the context of other tests + + /// + /// Documents used for integration tests + /// + private static readonly List 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 } - }; + ]; /// /// Add the test documents to the database @@ -194,143 +149,159 @@ public static class SqliteCSharpTests foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } - private static readonly Test Integration = TestList("Integration", new[] + /// + /// Integration tests for the Configuration module of the SQLite library + /// + private static readonly Test ConfigurationTests = TestCase("Configuration.UseConnectionString succeeds", () => { - TestCase("Configuration.UseConnectionString succeeds", () => + try { - try + Sqlite.Configuration.UseConnectionString("Data Source=test.db"); + Expect.equal(Sqlite.Configuration.connectionString, + new FSharpOption("Data Source=test.db;Foreign Keys=True"), "Connection string incorrect"); + } + finally + { + Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); + } + }); + + /// + /// Integration tests for the Custom module of the SQLite library + /// + private static readonly Test CustomTests = TestList("Custom", + [ + TestList("Single", + [ + TestCase("succeeds when a row is found", async () => { - Sqlite.Configuration.UseConnectionString("Data Source=test.db"); - Expect.equal(Sqlite.Configuration.connectionString, - new FSharpOption("Data Source=test.db;Foreign Keys=True"), "Connection string incorrect"); - } - finally + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", + [Parameters.Id("one")], Results.FromData); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "one", "The incorrect document was returned"); + }), + TestCase("succeeds when a row is not found", async () => { - Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", + [Parameters.Id("eighty")], Results.FromData); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("List", + [ + TestCase("succeeds when data is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List(Query.Find(SqliteDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); + }), + TestCase("succeeds when data is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List( + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", [new("@value", 100)], + Results.FromData); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("NonQuery", + [ + TestCase("succeeds when operating on data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); + + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); + }), + TestCase("succeeds when no data matches where clause", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", + [new("@value", 100)]); + + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); + }) + ]), + TestCase("Scalar succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }) + ]); + + /// + /// Integration tests for the Definition module of the SQLite library + /// + private static readonly Test DefinitionTests = TestList("Definition", + [ + TestCase("EnsureTable succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var exists = await ItExists("ensured"); + var alsoExists = await ItExists("idx_ensured_key"); + Expect.isFalse(exists, "The table should not exist already"); + Expect.isFalse(alsoExists, "The key index should not exist already"); + + await Definition.EnsureTable("ensured"); + + exists = await ItExists("ensured"); + alsoExists = await ItExists("idx_ensured_key"); + Expect.isTrue(exists, "The table should now exist"); + Expect.isTrue(alsoExists, "The key index should now exist"); + return; + + async ValueTask ItExists(string name) + { + return await Custom.Scalar($"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + [new("@name", name)], Results.ToExists); } }), - TestList("Custom", new[] + TestCase("EnsureFieldIndex succeeds", async () => { - TestList("Single", new[] - { - TestCase("succeeds when a row is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); - var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("one") }, Results.FromData); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "one", "The incorrect document was returned"); - }), - TestCase("succeeds when a row is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + var exists = await IndexExists(); + Expect.isFalse(exists, "The index should not exist already"); - var doc = await Custom.Single($"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", - new[] { Parameters.Id("eighty") }, Results.FromData); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("List", new[] - { - TestCase("succeeds when data is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await Definition.EnsureTable("ensured"); + await Definition.EnsureFieldIndex("ensured", "test", ["Id", "Category"]); + exists = await IndexExists(); + Expect.isTrue(exists, "The index should now exist"); + return; - var docs = await Custom.List(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, - Results.FromData); - Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); - }), - TestCase("succeeds when data is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + Task IndexExists() => Custom.Scalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", + Parameters.None, Results.ToExists); + }) + ]); - var docs = await Custom.List( - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }, Results.FromData); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("NonQuery", new[] - { - TestCase("succeeds when operating on data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); - - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); - }), - TestCase("succeeds when no data matches where clause", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Custom.NonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@value", 100) }); - - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); - }) - }), - TestCase("Scalar succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var nbr = await Custom.Scalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); - Expect.equal(nbr, 5, "The query should have returned the number 5"); - }) - }), - TestList("Definition", new[] - { - TestCase("EnsureTable succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var exists = await ItExists("ensured"); - var alsoExists = await ItExists("idx_ensured_key"); - Expect.isFalse(exists, "The table should not exist already"); - Expect.isFalse(alsoExists, "The key index should not exist already"); - - await Definition.EnsureTable("ensured"); - - exists = await ItExists("ensured"); - alsoExists = await ItExists("idx_ensured_key"); - Expect.isTrue(exists, "The table should now exist"); - Expect.isTrue(alsoExists, "The key index should now exist"); - return; - - async ValueTask ItExists(string name) - { - return await Custom.Scalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, Results.ToExists); - } - }), - TestCase("EnsureFieldIndex succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - var indexExists = () => Custom.Scalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it", - Parameters.None, Results.ToExists); - - var exists = await indexExists(); - Expect.isFalse(exists, "The index should not exist already"); - - await Definition.EnsureTable("ensured"); - await Definition.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); - exists = await indexExists(); - Expect.isTrue(exists, "The index should now exist"); - }) - }), - TestList("Document.Insert", new[] - { + /// + /// Integration tests for the Document module of the SQLite library + /// + private static readonly Test DocumentTests = TestList("Document", + [ + TestList("Insert", + [ TestCase("succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -354,10 +325,87 @@ public static class SqliteCSharpTests { // This is what is supposed to happen } + }), + TestCase("succeeds when adding a numeric auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } + }), + TestCase("succeeds when adding a GUID auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Guid); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) - }), - TestList("Document.Save", new[] - { + ]), + TestList("Save", + [ TestCase("succeeds when a document is inserted", async () => { await using var db = await SqliteDb.BuildDb(); @@ -388,385 +436,522 @@ public static class SqliteCSharpTests Expect.equal(after!.Id, "test", "The updated document is not correct"); Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); }) - }), - TestList("Count", new[] + ]) + ]); + + /// + /// Integration tests for the Count module of the SQLite library + /// + private static readonly Test CountTests = TestList("Count", + [ + TestCase("All succeeds", async () => { - TestCase("All succeeds", async () => + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.All(SqliteDb.TableName); + Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + }), + TestList("ByFields", + [ + TestCase("succeeds for numeric range", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var theCount = await Count.All(SqliteDb.TableName); - Expect.equal(theCount, 5L, "There should have been 5 matching documents"); - }), - TestCase("ByField succeeds for numeric range", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("NumValue", 10, 20)); + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.BT("NumValue", 10, 20)]); Expect.equal(theCount, 3L, "There should have been 3 matching documents"); }), - TestCase("ByField succeeds for non-numeric range", async () => + TestCase("succeeds for non-numeric range", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var theCount = await Count.ByField(SqliteDb.TableName, Field.BT("Value", "aardvark", "apple")); + var theCount = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.BT("Value", "aardvark", "apple")]); Expect.equal(theCount, 1L, "There should have been 1 matching document"); }) - }), - TestList("Exists", new[] - { - TestList("ById", new[] + ]) + ]); + + /// + /// Integration tests for the Exists module of the SQLite library + /// + private static readonly Test ExistsTests = TestList("Exists", + [ + TestList("ById", + [ + TestCase("succeeds when a document exists", async () => { - TestCase("succeeds when a document exists", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var exists = await Exists.ById(SqliteDb.TableName, "three"); - Expect.isTrue(exists, "There should have been an existing document"); - }), - TestCase("succeeds when a document does not exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ById(SqliteDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) + var exists = await Exists.ById(SqliteDb.TableName, "three"); + Expect.isTrue(exists, "There should have been an existing document"); }), - TestList("ByField", new[] + TestCase("succeeds when a document does not exist", async () => { - TestCase("succeeds when documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var exists = await Exists.ByField(SqliteDb.TableName, Field.GE("NumValue", 10)); - Expect.isTrue(exists, "There should have been existing documents"); - }), - TestCase("succeeds when no matching documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var exists = await Exists.ByField(SqliteDb.TableName, Field.EQ("Nothing", "none")); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) + var exists = await Exists.ById(SqliteDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); }) - }), - TestList("Find", new[] - { - TestList("All", new[] + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents exist", async () => { - TestCase("succeeds when there is data", async () => - { - await using var db = await SqliteDb.BuildDb(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); - - var results = await Find.All(SqliteDb.TableName); - Expect.equal(results.Count, 3, "There should have been 3 documents returned"); - }), - TestCase("succeeds when there is no data", async () => - { - await using var db = await SqliteDb.BuildDb(); - var results = await Find.All(SqliteDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.GE("NumValue", 10)]); + Expect.isTrue(exists, "There should have been existing documents"); }), - TestList("ById", new[] + TestCase("succeeds when no matching documents exist", async () => { - TestCase("succeeds when a document is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - var doc = await Find.ById(SqliteDb.TableName, "two"); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.ById(SqliteDb.TableName, "twenty two"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when documents are found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByField(SqliteDb.TableName, Field.GT("NumValue", 15)); - Expect.equal(docs.Count, 2, "There should have been two documents returned"); - }), - TestCase("succeeds when documents are not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var docs = await Find.ByField(SqliteDb.TableName, Field.EQ("Value", "mauve")); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("FirstByField", new[] - { - TestCase("succeeds when a document is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Value", "another")); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.equal(doc!.Id, "two", "The incorrect document was returned"); - }), - TestCase("succeeds when multiple documents are found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Sub.Foo", "green")); - Expect.isNotNull(doc, "There should have been a document returned"); - Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); - }), - TestCase("succeeds when a document is not found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - var doc = await Find.FirstByField(SqliteDb.TableName, Field.EQ("Value", "absent")); - Expect.isNull(doc, "There should not have been a document returned"); - }) + var exists = await Exists.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Nothing", "none")]); + Expect.isFalse(exists, "There should not have been any existing documents"); }) - }), - TestList("Update", new[] - { - TestList("ById", new[] + ]) + ]); + + /// + /// Integration tests for the Find module of the SQLite library + /// + private static readonly Test FindTests = TestList("Find", + [ + TestList("All", + [ + TestCase("succeeds when there is data", async () => { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); - var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await Update.ById(SqliteDb.TableName, "one", testDoc); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is not correct"); - Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); - Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct"); - Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ById(SqliteDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) + var results = await Find.All(SqliteDb.TableName); + Expect.equal(results.Count, 3, "There should have been 3 documents returned"); }), - TestList("ByFunc", new[] + TestCase("succeeds when there is no data", async () => { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is incorrect"); - Expect.equal(after.Value, "le un", "The updated document is incorrect"); - Expect.equal(after.NumValue, 1, "The updated document is incorrect"); - Expect.isNull(after.Sub, "The updated document should not have a sub-document"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - }), - }), - TestList("Patch", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Patch.ById(SqliteDb.TableName, "one", new { NumValue = 44 }); - var after = await Find.ById(SqliteDb.TableName, "one"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - Expect.equal(after!.Id, "one", "The updated document is not correct"); - Expect.equal(after.NumValue, 44, "The updated document is not correct"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "purple"), new { NumValue = 77 }); - var after = await Count.ByField(SqliteDb.TableName, Field.EQ("NumValue", 77)); - Expect.equal(after, 2L, "There should have been 2 documents returned"); - }), - TestCase("succeeds when no document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await Patch.ByField(SqliteDb.TableName, Field.EQ("Value", "burgundy"), new { Foo = "green" }); - }) + await using var db = await SqliteDb.BuildDb(); + var results = await Find.All(SqliteDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); }) - }), - TestList("RemoveFields", new[] - { - TestList("ById", new[] + ]), + TestList("AllOrdered", + [ + TestCase("succeeds when ordering numerically", async () => { - TestCase("succeeds when fields are removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Sub", "Value" }); - var updated = await Find.ById(SqliteDb.TableName, "two"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.equal(updated.Value, "", "The string value should have been removed"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "AFieldThatIsNotThere" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = await SqliteDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ById(SqliteDb.TableName, "two", new[] { "Value" }); - }) + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "one|three|two|four|five", + "The documents were not ordered correctly"); }), - TestList("ByField", new[] + TestCase("succeeds when ordering numerically descending", async () => { - TestCase("succeeds when a field is removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - await RemoveFields.ByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Sub" }); - var updated = await Find.ById(SqliteDb.TableName, "four"); - Expect.isNotNull(updated, "The updated document should have been retrieved"); - Expect.isNull(updated.Sub, "The sub-document should have been removed"); - }), - TestCase("succeeds when a field is not removed", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - // This not raising an exception is the test - await RemoveFields.ByField(SqliteDb.TableName, Field.EQ("NumValue", 17), new[] { "Nothing" }); - }), - TestCase("succeeds when no document is matched", async () => - { - await using var db = await SqliteDb.BuildDb(); - - // This not raising an exception is the test - await RemoveFields.ByField(SqliteDb.TableName, Field.NE("Abracadabra", "apple"), new[] { "Value" }); - }) - }) - }), - TestList("Delete", new[] - { - TestList("ById", new[] - { - TestCase("succeeds when a document is deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(SqliteDb.TableName, "four"); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); - }), - TestCase("succeeds when a document is not deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ById(SqliteDb.TableName, "thirty"); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("n:NumValue DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "five|four|two|three|one", + "The documents were not ordered correctly"); }), - TestList("ByField", new[] + TestCase("succeeds when ordering alphabetically", async () => { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); - await Delete.ByField(SqliteDb.TableName, Field.NE("Value", "purple")); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); - }), - TestCase("succeeds when documents are not deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ByField(SqliteDb.TableName, Field.EQ("Value", "crimson")); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) + var results = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("Id DESC")]); + Expect.hasLength(results, 5, "There should have been 5 documents returned"); + Expect.equal(string.Join('|', results.Select(x => x.Id)), "two|three|one|four|five", + "The documents were not ordered correctly"); }) - }), - TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + ]), + TestList("ById", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + var doc = await Find.ById(SqliteDb.TableName, "two"); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.ById(SqliteDb.TableName, "twenty two"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)]); + Expect.equal(docs.Count, 2, "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "mauve")]); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + ]), + TestList("ByFieldsOrdered", + [ + TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four", + "There should have been two documents returned"); + }), + TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.GT("NumValue", 15)], [Field.Named("Id DESC")]); + Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five", + "There should have been two documents returned"); + }) + ]), + TestList("FirstByFields", + [ + TestCase("succeeds when a document is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "another")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + TestCase("succeeds when multiple documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.contains(["two", "four"], doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when a document is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFields(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Value", "absent")]); + Expect.isNull(doc, "There should not have been a document returned"); + }) + ]), + TestList("FirstByFieldsOrdered", + [ + TestCase("succeeds when sorting ascending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("two", doc!.Id, "An incorrect document was returned"); + }), + TestCase("succeeds when sorting descending", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByFieldsOrdered(SqliteDb.TableName, FieldMatch.Any, + [Field.EQ("Sub.Foo", "green")], [Field.Named("Sub.Bar DESC")]); + Expect.isNotNull(doc, "There should have been a document returned"); + Expect.equal("four", doc!.Id, "An incorrect document was returned"); + }) + ]) + ]); + + /// + /// Integration tests for the Update module of the SQLite library + /// + private static readonly Test UpdateTests = TestList("Update", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await Update.ById(SqliteDb.TableName, "one", testDoc); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is not correct"); + Expect.isNotNull(after.Sub, "The updated document should have had a sub-document"); + Expect.equal(after.Sub!.Foo, "blue", "The updated sub-document is not correct"); + Expect.equal(after.Sub.Bar, "red", "The updated sub-document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ById(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + ]), + TestList("ByFunc", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is incorrect"); + Expect.equal(after.Value, "le un", "The updated document is incorrect"); + Expect.equal(after.NumValue, 1, "The updated document is incorrect"); + Expect.isNull(after.Sub, "The updated document should not have a sub-document"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + ]), + ]); + + /// + /// Integration tests for the Patch module of the SQLite library + /// + private static readonly Test PatchTests = TestList("Patch", + [ + TestList("ById", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Patch.ById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + Expect.equal(after!.Id, "one", "The updated document is not correct"); + Expect.equal(after.NumValue, 44, "The updated document is not correct"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "purple")], + new { NumValue = 77 }); + var after = await Count.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 77)]); + Expect.equal(after, 2L, "There should have been 2 documents returned"); + }), + TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await Patch.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("Value", "burgundy")], + new { Foo = "green" }); + }) + ]) + ]); + + /// + /// Integration tests for the RemoveFields module of the SQLite library + /// + private static readonly Test RemoveFieldsTests = TestList("RemoveFields", + [ + TestList("ById", + [ + TestCase("succeeds when fields are removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ById(SqliteDb.TableName, "two", ["Sub", "Value"]); + var updated = await Find.ById(SqliteDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.equal(updated.Value, "", "The string value should have been removed"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ById(SqliteDb.TableName, "two", ["AFieldThatIsNotThere"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = await SqliteDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ById(SqliteDb.TableName, "two", ["Value"]); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when a field is removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], ["Sub"]); + var updated = await Find.ById(SqliteDb.TableName, "four"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.EQ("NumValue", 17)], + ["Nothing"]); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = await SqliteDb.BuildDb(); + + // This not raising an exception is the test + await RemoveFields.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Abracadabra", "apple")], + ["Value"]); + }) + ]) + ]); + + /// + /// Integration tests for the Delete module of the SQLite library + /// + private static readonly Test DeleteTests = TestList("Delete", + [ + TestList("ById", + [ + TestCase("succeeds when a document is deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(SqliteDb.TableName, "four"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); + }), + TestCase("succeeds when a document is not deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ById(SqliteDb.TableName, "thirty"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + ]), + TestList("ByFields", + [ + TestCase("succeeds when documents are deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(SqliteDb.TableName, FieldMatch.Any, [Field.NE("Value", "purple")]); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByFields(SqliteDb.TableName, FieldMatch.All, [Field.EQ("Value", "crimson")]); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + ]) + ]); + /// /// All tests for SQLite C# functions and methods /// [Tests] - public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) }); + public static readonly Test All = TestList("Sqlite.C#", + [ + TestList("Unit", [QueryTests, ParametersTests]), + TestSequenced(TestList("Integration", + [ + ConfigurationTests, + CustomTests, + DefinitionTests, + DocumentTests, + CountTests, + ExistsTests, + FindTests, + UpdateTests, + PatchTests, + RemoveFieldsTests, + DeleteTests, + TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) + ])) + ]); } diff --git a/src/Tests.CSharp/Types.cs b/src/Tests.CSharp/Types.cs index 5e7f972..ce63230 100644 --- a/src/Tests.CSharp/Types.cs +++ b/src/Tests.CSharp/Types.cs @@ -1,5 +1,11 @@ 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; } = ""; diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index 90c8b65..a362ea2 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -2,11 +2,12 @@ Exe + 1182 - + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index c5265ea..ad0da18 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -6,135 +6,460 @@ open Expecto /// Test table name let tbl = "test_table" -/// 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 "GT succeeds" { - Expect.equal (string GT) ">" "The greater than operator was not correct" - } - test "GE succeeds" { - Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct" - } - test "LT succeeds" { - Expect.equal (string LT) "<" "The less than operator was not correct" - } - test "LE succeeds" { - Expect.equal (string LE) "<=" "The less than or equal to operator was not correct" - } - test "NE succeeds" { - Expect.equal (string NE) "<>" "The not equal to operator was not correct" - } - test "BT succeeds" { - Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct""" - } - test "EX succeeds" { - Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" - } - test "NEX succeeds" { - Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" - } - ] - testList "Field" [ - test "EQ succeeds" { - let field = Field.EQ "Test" 14 - Expect.equal field.Name "Test" "Field name incorrect" - Expect.equal field.Op EQ "Operator incorrect" - Expect.equal field.Value 14 "Value incorrect" - } - test "GT succeeds" { - let field = Field.GT "Great" "night" - Expect.equal field.Name "Great" "Field name incorrect" - Expect.equal field.Op GT "Operator incorrect" - Expect.equal field.Value "night" "Value incorrect" - } - test "GE succeeds" { - let field = Field.GE "Nice" 88L - Expect.equal field.Name "Nice" "Field name incorrect" - Expect.equal field.Op GE "Operator incorrect" - Expect.equal field.Value 88L "Value incorrect" - } - test "LT succeeds" { - let field = Field.LT "Lesser" "seven" - Expect.equal field.Name "Lesser" "Field name incorrect" - Expect.equal field.Op LT "Operator incorrect" - Expect.equal field.Value "seven" "Value incorrect" - } - test "LE succeeds" { - let field = Field.LE "Nobody" "KNOWS"; - Expect.equal field.Name "Nobody" "Field name incorrect" - Expect.equal field.Op LE "Operator incorrect" - Expect.equal field.Value "KNOWS" "Value incorrect" - } - test "NE succeeds" { - let field = Field.NE "Park" "here" - Expect.equal field.Name "Park" "Field name incorrect" - Expect.equal field.Op NE "Operator incorrect" - Expect.equal field.Value "here" "Value incorrect" - } - test "BT succeeds" { - let field = Field.BT "Age" 18 49 - Expect.equal field.Name "Age" "Field name incorrect" - Expect.equal field.Op BT "Operator incorrect" - Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" - } - test "EX succeeds" { - let field = Field.EX "Groovy" - Expect.equal field.Name "Groovy" "Field name incorrect" - Expect.equal field.Op EX "Operator incorrect" - } - test "NEX succeeds" { - let field = Field.NEX "Rad" - Expect.equal field.Name "Rad" "Field name incorrect" - Expect.equal field.Op NEX "Operator incorrect" - } - ] - testList "Query" [ - test "selectFromTable succeeds" { - Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" - } - testList "Definition" [ - test "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" +/// Unit tests for the Op DU +let opTests = testList "Op" [ + test "EQ succeeds" { + Expect.equal (string EQ) "=" "The equals operator was not correct" + } + test "GT succeeds" { + Expect.equal (string GT) ">" "The greater than operator was not correct" + } + test "GE succeeds" { + Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct" + } + test "LT succeeds" { + Expect.equal (string LT) "<" "The less than operator was not correct" + } + test "LE succeeds" { + Expect.equal (string LE) "<=" "The less than or equal to operator was not correct" + } + test "NE succeeds" { + Expect.equal (string NE) "<>" "The not equal to operator was not correct" + } + test "BT succeeds" { + Expect.equal (string BT) "BETWEEN" """The "between" operator was not correct""" + } + test "EX succeeds" { + Expect.equal (string EX) "IS NOT NULL" """The "exists" operator was not correct""" + } + test "NEX succeeds" { + Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" + } +] + +/// Unit tests for the Field class +let fieldTests = testList "Field" [ + test "EQ succeeds" { + let field = Field.EQ "Test" 14 + Expect.equal field.Name "Test" "Field name incorrect" + Expect.equal field.Op EQ "Operator incorrect" + Expect.equal field.Value 14 "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "GT succeeds" { + let field = Field.GT "Great" "night" + Expect.equal field.Name "Great" "Field name incorrect" + Expect.equal field.Op GT "Operator incorrect" + Expect.equal field.Value "night" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "GE succeeds" { + let field = Field.GE "Nice" 88L + Expect.equal field.Name "Nice" "Field name incorrect" + Expect.equal field.Op GE "Operator incorrect" + Expect.equal field.Value 88L "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "LT succeeds" { + let field = Field.LT "Lesser" "seven" + Expect.equal field.Name "Lesser" "Field name incorrect" + Expect.equal field.Op LT "Operator incorrect" + Expect.equal field.Value "seven" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "LE succeeds" { + let field = Field.LE "Nobody" "KNOWS"; + Expect.equal field.Name "Nobody" "Field name incorrect" + Expect.equal field.Op LE "Operator incorrect" + Expect.equal field.Value "KNOWS" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "NE succeeds" { + let field = Field.NE "Park" "here" + Expect.equal field.Name "Park" "Field name incorrect" + Expect.equal field.Op NE "Operator incorrect" + Expect.equal field.Value "here" "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "BT succeeds" { + let field = Field.BT "Age" 18 49 + Expect.equal field.Name "Age" "Field name incorrect" + Expect.equal field.Op BT "Operator incorrect" + Expect.sequenceEqual (field.Value :?> obj list) [ 18; 49 ] "Value incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "EX succeeds" { + let field = Field.EX "Groovy" + Expect.equal field.Name "Groovy" "Field name incorrect" + Expect.equal field.Op EX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + test "NEX succeeds" { + let field = Field.NEX "Rad" + Expect.equal field.Name "Rad" "Field name incorrect" + Expect.equal field.Op NEX "Operator incorrect" + Expect.isNone field.ParameterName "The default parameter name should be None" + Expect.isNone field.Qualifier "The default table qualifier should be None" + } + testList "NameToPath" [ + test "succeeds for PostgreSQL and a simple name" { + Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly" + } + test "succeeds for SQLite and a simple name" { + Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" SQLite) "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) + "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) + "Path not constructed correctly" + } + ] + test "WithParameterName succeeds" { + let field = (Field.EQ "Bob" "Tom").WithParameterName "@name" + Expect.isSome field.ParameterName "The parameter name should have been filled" + Expect.equal "@name" field.ParameterName.Value "The parameter name is incorrect" + } + test "WithQualifier succeeds" { + let field = (Field.EQ "Bill" "Matt").WithQualifier "joe" + Expect.isSome field.Qualifier "The table qualifier should have been filled" + Expect.equal "joe" field.Qualifier.Value "The table qualifier is incorrect" + } + testList "Path" [ + test "succeeds for a PostgreSQL single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data#>>'{My,Nested,Field}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a PostgreSQL nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data#>>'{Nest,Away}'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect" + } + test "succeeds for a SQLite single field with no qualifier" { + let field = Field.GE "SomethingCool" 18 + Expect.equal "data->>'SomethingCool'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite single field with a qualifier" { + let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" } + Expect.equal "this.data->>'SomethingElse'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite nested field with no qualifier" { + let field = Field.EQ "My.Nested.Field" "howdy" + Expect.equal "data->>'My'->>'Nested'->>'Field'" (field.Path SQLite) "The SQLite path is incorrect" + } + test "succeeds for a SQLite nested field with a qualifier" { + let field = { Field.EQ "Nest.Away" "doc" with Qualifier = Some "bird" } + Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect" + } + ] +] + +/// 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 + (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 + (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 + (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 + (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> } - testList "ensureKey" [ - test "succeeds when a schema is present" { - 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" - } - test "succeeds when a schema is not present" { - 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" - } - ] - test "ensureIndexOn succeeds for multiple fields and directions" { - Expect.equal - (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)" ] - |> String.concat "") - "CREATE INDEX for multiple field statement incorrect" - } - ] - test "insert succeeds" { - Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" - } - test "save succeeds" { + + let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} + Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" + + let deserialized = Configuration.serializer().Deserialize """{"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 "Definition" [ + test "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" [ + test "succeeds when a schema is present" { 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" + (Query.Definition.ensureKey "test.table" PostgreSQL) + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))" + "CREATE INDEX for key statement with schema not constructed correctly" + } + test "succeeds when a schema is not present" { + Expect.equal + (Query.Definition.ensureKey "table" SQLite) + "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" { + Expect.equal + (Query.Definition.ensureIndexOn + "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ] PostgreSQL) + ([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" ] + |> String.concat "") + "CREATE INDEX for multiple field statement incorrect" + } + test "succeeds for nested PostgreSQL field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] PostgreSQL) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data#>>'{{a,b,c}}'))" + "CREATE INDEX for nested PostgreSQL field incorrect" + } + test "succeeds for nested SQLite field" { + Expect.equal + (Query.Definition.ensureIndexOn tbl "nest" [ "a.b.c" ] SQLite) + $"CREATE INDEX IF NOT EXISTS idx_{tbl}_nest ON {tbl} ((data->>'a'->>'b'->>'c'))" + "CREATE INDEX for nested SQLite field incorrect" } ] ] + test "insert succeeds" { + Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" + } + test "save succeeds" { + 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" + } + 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" + } + ] +] +/// Tests which do not hit the database +let all = testList "Common" [ + opTests + fieldTests + fieldMatchTests + parameterNameTests + autoIdTests + queryTests + testSequenced configurationTests +] diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs index 41b1210..533cf12 100644 --- a/src/Tests/PostgresExtensionTests.fs +++ b/src/Tests/PostgresExtensionTests.fs @@ -25,7 +25,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! docs = conn.customList (Query.selectFromTable PostgresDb.TableName) [] fromData + let! docs = conn.customList (Query.find PostgresDb.TableName) [] fromData Expect.equal (List.length docs) 5 "There should have been 5 documents returned" } testTask "succeeds when data is not found" { @@ -209,12 +209,12 @@ let integrationTests = let! theCount = conn.countAll PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } - testTask "countByField succeeds" { + testTask "countByFields succeeds" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! theCount = conn.countByField PostgresDb.TableName (Field.EQ "Value" "purple") + let! theCount = conn.countByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.equal theCount 2 "There should have been 2 matching documents" } testTask "countByContains succeeds" { @@ -251,13 +251,13 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "existsByField" [ + testList "existsByFields" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! exists = conn.existsByField PostgresDb.TableName (Field.EX "Sub") + let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EX "Sub" ] Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when documents do not exist" { @@ -265,7 +265,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! exists = conn.existsByField PostgresDb.TableName (Field.EQ "NumValue" "six") + let! exists = conn.existsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "six" ] Expect.isFalse exists "There should not have been existing documents" } ] @@ -315,12 +315,10 @@ let integrationTests = do! conn.insert PostgresDb.TableName { Foo = "five"; Bar = "six" } let! results = conn.findAll PostgresDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" } testTask "succeeds when there is no data" { use db = PostgresDb.BuildDb() @@ -329,6 +327,44 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] + testList "findAllOrdered" [ + testTask "succeeds when ordering numerically" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! results = conn.findAllOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "findById" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -348,13 +384,13 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] - testList "findByField" [ + testList "findByFields" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! docs = conn.findByField PostgresDb.TableName (Field.EQ "Value" "another") + let! docs = conn.findByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] Expect.equal (List.length docs) 1 "There should have been one document returned" } testTask "succeeds when documents are not found" { @@ -362,10 +398,36 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! docs = conn.findByField PostgresDb.TableName (Field.EQ "Value" "mauve") + let! docs = conn.findByFields PostgresDb.TableName Any [ Field.EQ "Value" "mauve" ] Expect.isEmpty docs "There should have been no documents returned" } ] + testList "findByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly" + } + ] testList "findByContains" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() @@ -384,6 +446,33 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] + testList "findByContainsOrdered" [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|two" "Documents not ordered correctly" + } + ] testList "findByJsonPath" [ testTask "succeeds when documents are found" { use db = PostgresDb.BuildDb() @@ -402,13 +491,40 @@ let integrationTests = Expect.isEmpty docs "There should have been no documents returned" } ] - testList "findFirstByField" [ + testList "findByJsonPathOrdered" [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "one|three|two" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.findByJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue DESC" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|three|one" "Documents not ordered correctly" + } + ] + testList "findFirstByFields" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "another") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } @@ -417,7 +533,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "purple") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.isSome doc "There should have been a document returned" Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" } @@ -426,10 +542,34 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - let! doc = conn.findFirstByField PostgresDb.TableName (Field.EQ "Value" "absent") + let! doc = conn.findFirstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "five" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "findFirstByContains" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -458,6 +598,30 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByContainsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "findFirstByJsonPath" [ testTask "succeeds when a document is found" { use db = PostgresDb.BuildDb() @@ -486,6 +650,30 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] + testList "findFirstByJsonPathOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.findFirstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] testList "updateById" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() @@ -556,14 +744,14 @@ let integrationTests = do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |} } ] - testList "patchByField" [ + testList "patchByFields" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = conn.countByField PostgresDb.TableName (Field.EQ "NumValue" "77") + do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = conn.countByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "77" ] Expect.equal after 2 "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -573,7 +761,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.patchByField PostgresDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! conn.patchByFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] testList "patchByContains" [ @@ -623,9 +811,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -634,9 +822,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsById PostgresDb.TableName "two" [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -655,16 +843,16 @@ let integrationTests = do! conn.removeFieldsById PostgresDb.TableName "two" [ "Value" ] } ] - testList "removeFieldsByField" [ + testList "removeFieldsByFields" [ testTask "succeeds when multiple fields are removed" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -672,10 +860,10 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -684,14 +872,14 @@ let integrationTests = do! loadDocs conn // This not raising an exception is the test - do! conn.removeFieldsByField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Nothing" ] + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use db = PostgresDb.BuildDb() use conn = mkConn db // This not raising an exception is the test - do! conn.removeFieldsByField PostgresDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! conn.removeFieldsByFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] testList "removeFieldsByContains" [ @@ -701,9 +889,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -712,9 +900,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -740,9 +928,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 1 "There should be 1 document without Value fields" } testTask "succeeds when a single field is removed" { @@ -751,9 +939,9 @@ let integrationTests = do! loadDocs conn do! conn.removeFieldsByJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] - let! noSubs = conn.countByField PostgresDb.TableName (Field.NEX "Sub") + let! noSubs = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Sub" ] Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = conn.countByField PostgresDb.TableName (Field.NEX "Value") + let! noValue = conn.countByFields PostgresDb.TableName Any [ Field.NEX "Value" ] Expect.equal noValue 0 "There should be no documents without Value fields" } testTask "succeeds when a field is not removed" { @@ -792,13 +980,13 @@ let integrationTests = Expect.equal remaining 5 "There should have been 5 documents remaining" } ] - testList "deleteByField" [ + testList "deleteByFields" [ testTask "succeeds when documents are deleted" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "purple") + do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] let! remaining = conn.countAll PostgresDb.TableName Expect.equal remaining 3 "There should have been 3 documents remaining" } @@ -807,7 +995,7 @@ let integrationTests = use conn = mkConn db do! loadDocs conn - do! conn.deleteByField PostgresDb.TableName (Field.EQ "Value" "crimson") + do! conn.deleteByFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = conn.countAll PostgresDb.TableName Expect.equal remaining 5 "There should have been 5 documents remaining" } diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 6849b8b..497e712 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -5,1096 +5,1334 @@ open BitBadger.Documents open BitBadger.Documents.Postgres open BitBadger.Documents.Tests -/// Tests which do not hit the database -let unitTests = - testList "Unit" [ - testList "Parameters" [ - test "idParam succeeds" { - Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly" - } - test "jsonParam succeeds" { - Expect.equal - (jsonParam "@test" {| Something = "good" |}) - ("@test", Sql.jsonb """{"Something":"good"}""") - "JSON parameter not constructed correctly" - } - testList "addFieldParam" [ - test "succeeds when a parameter is added" { - let paramList = addFieldParam "@field" (Field.EQ "it" "242") [] - Expect.hasLength paramList 1 "There should have been a parameter added" - let it = paramList[0] - Expect.equal (fst it) "@field" "Field parameter name not correct" - match snd it with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@field" "Parameter name not correct" - Expect.equal value.Value "242" "Parameter value not correct" - | _ -> Expect.isTrue false "The parameter was not a Parameter type" - } - test "succeeds when a parameter is not added" { - let paramList = addFieldParam "@field" (Field.EX "tacos") [] - Expect.isEmpty paramList "There should not have been any parameters added" - } - test "succeeds when two parameters are added" { - let paramList = addFieldParam "@field" (Field.BT "that" "eh" "zed") [] - Expect.hasLength paramList 2 "There should have been 2 parameters added" - let min = paramList[0] - Expect.equal (fst min) "@fieldmin" "Minimum field name not correct" - match snd min with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@fieldmin" "Minimum parameter name not correct" - Expect.equal value.Value "eh" "Minimum parameter value not correct" - | _ -> Expect.isTrue false "Minimum parameter was not a Parameter type" - let max = paramList[1] - Expect.equal (fst max) "@fieldmax" "Maximum field name not correct" - match snd max with - | SqlValue.Parameter value -> - Expect.equal value.ParameterName "@fieldmax" "Maximum parameter name not correct" - Expect.equal value.Value "zed" "Maximum parameter value not correct" - | _ -> Expect.isTrue false "Maximum parameter was not a Parameter type" - } - ] - test "noParams succeeds" { - Expect.isEmpty noParams "The no-params sequence should be empty" - } - ] - testList "Query" [ - testList "whereByField" [ - test "succeeds when a logical operator is passed" { - Expect.equal - (Query.whereByField (Field.GT "theField" 0) "@test") - "data->>'theField' > @test" - "WHERE clause not correct" - } - test "succeeds when an existence operator is passed" { - Expect.equal - (Query.whereByField (Field.NEX "thatField") "") - "data->>'thatField' IS NULL" - "WHERE clause not correct" - } - test "succeeds when a between operator is passed with numeric values" { - Expect.equal - (Query.whereByField (Field.BT "aField" 50 99) "@range") - "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax" - "WHERE clause not correct" - } - test "succeeds when a between operator is passed with non-numeric values" { - Expect.equal - (Query.whereByField (Field.BT "field0" "a" "b") "@alpha") - "data->>'field0' BETWEEN @alphamin AND @alphamax" - "WHERE clause not correct" - } - ] - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" - } - testList "Definition" [ - test "ensureTable succeeds" { - Expect.equal - (Query.Definition.ensureTable PostgresDb.TableName) - $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" - "CREATE TABLE statement not constructed correctly" - } - test "ensureDocumentIndex succeeds for full index" { - Expect.equal - (Query.Definition.ensureDocumentIndex "schema.tbl" Full) - "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)" - "CREATE INDEX statement not constructed correctly" - } - test "ensureDocumentIndex succeeds for JSONB Path Ops index" { - Expect.equal - (Query.Definition.ensureDocumentIndex PostgresDb.TableName Optimized) - (sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)" - PostgresDb.TableName PostgresDb.TableName) - "CREATE INDEX statement not constructed correctly" - } - ] - test "update succeeds" { - Expect.equal - (Query.update PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = @data WHERE data->>'Id' = @id" - "UPDATE full statement not correct" - } - test "whereDataContains succeeds" { - Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" - } - test "whereJsonPathMatches succeeds" { - Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" - } - testList "Count" [ - test "all succeeds" { - Expect.equal - (Query.Count.all PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}" - "Count query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Count.byField PostgresDb.TableName (Field.EQ "thatField" 0)) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data->>'thatField' = @field" - "JSON field text comparison count query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Count.byContains PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria" - "JSON containment count query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Count.byJsonPath PostgresDb.TableName) - $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "JSON Path match count query not correct" - } - ] - testList "Exists" [ - test "byId succeeds" { - Expect.equal - (Query.Exists.byId PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Id' = @id) AS it" - "ID existence query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Exists.byField PostgresDb.TableName (Field.LT "Test" 0)) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data->>'Test' < @field) AS it" - "JSON field text comparison exists query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Exists.byContains PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it" - "JSON containment exists query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Exists.byJsonPath PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it" - "JSON Path match existence query not correct" - } - ] - testList "Find" [ - test "byId succeeds" { - Expect.equal - (Query.Find.byId PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" - "SELECT by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Find.byField PostgresDb.TableName (Field.GE "Golf" 0)) - $"SELECT data FROM {PostgresDb.TableName} WHERE data->>'Golf' >= @field" - "SELECT by JSON comparison query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Find.byContains PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria" - "SELECT by JSON containment query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Find.byJsonPath PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "SELECT by JSON Path match query not correct" - } - ] - testList "Patch" [ - test "byId succeeds" { - Expect.equal - (Query.Patch.byId PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Patch.byField PostgresDb.TableName (Field.LT "Snail" 0)) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data->>'Snail' < @field" - "UPDATE partial by ID statement not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.Patch.byContains PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" - "UPDATE partial by JSON containment statement not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.Patch.byJsonPath PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" - "UPDATE partial by JSON Path statement not correct" - } - ] - testList "RemoveFields" [ - test "byId succeeds" { - Expect.equal - (Query.RemoveFields.byId "tbl") - "UPDATE tbl SET data = data - @name WHERE data->>'Id' = @id" - "Remove field by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.RemoveFields.byField "tbl" (Field.LT "Fly" 0)) - "UPDATE tbl SET data = data - @name WHERE data->>'Fly' < @field" - "Remove field by field query not correct" - } - test "byContains succeeds" { - Expect.equal - (Query.RemoveFields.byContains "tbl") - "UPDATE tbl SET data = data - @name WHERE data @> @criteria" - "Remove field by contains query not correct" - } - test "byJsonPath succeeds" { - Expect.equal - (Query.RemoveFields.byJsonPath "tbl") - "UPDATE tbl SET data = data - @name WHERE data @? @path::jsonpath" - "Remove field by JSON path query not correct" - } - ] - testList "Delete" [ - test "byId succeeds" { - Expect.equal - (Query.Delete.byId PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'Id' = @id" - "DELETE by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Delete.byField PostgresDb.TableName (Field.NEX "gone")) - $"DELETE FROM {PostgresDb.TableName} WHERE data->>'gone' IS NULL" - "DELETE by JSON comparison query not correct" - } - test "byContains succeeds" { - Expect.equal (Query.Delete.byContains PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" - "DELETE by JSON containment query not correct" - } - test "byJsonPath succeeds" { - Expect.equal (Query.Delete.byJsonPath PostgresDb.TableName) - $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - "DELETE by JSON Path match query not correct" - } - ] - ] +(** UNIT TESTS **) + +/// Unit tests for the Parameters module of the PostgreSQL library +let parametersTests = testList "Parameters" [ + testList "idParam" [ + // NOTE: these tests also exercise all branches of the internal parameterFor function + test "succeeds for byte ID" { + Expect.equal (idParam (sbyte 7)) ("@id", Sql.int8 (sbyte 7)) "Byte ID parameter not constructed correctly" + } + test "succeeds for unsigned byte ID" { + Expect.equal + (idParam (byte 7)) + ("@id", Sql.int8 (int8 (byte 7))) + "Unsigned byte ID parameter not constructed correctly" + } + test "succeeds for short ID" { + Expect.equal + (idParam (int16 44)) ("@id", Sql.int16 (int16 44)) "Short ID parameter not constructed correctly" + } + test "succeeds for unsigned short ID" { + Expect.equal + (idParam (uint16 64)) + ("@id", Sql.int16 (int16 64)) + "Unsigned short ID parameter not constructed correctly" + } + test "succeeds for integer ID" { + Expect.equal (idParam 88) ("@id", Sql.int 88) "Int ID parameter not constructed correctly" + } + test "succeeds for unsigned integer ID" { + Expect.equal (idParam (uint 889)) ("@id", Sql.int 889) "Unsigned int ID parameter not constructed correctly" + } + test "succeeds for long ID" { + Expect.equal + (idParam (int64 123)) ("@id", Sql.int64 (int64 123)) "Long ID parameter not constructed correctly" + } + test "succeeds for unsigned long ID" { + Expect.equal + (idParam (uint64 6464)) + ("@id", Sql.int64 (int64 6464)) + "Unsigned long ID parameter not constructed correctly" + } + test "succeeds for decimal ID" { + Expect.equal + (idParam (decimal 4.56)) + ("@id", Sql.decimal (decimal 4.56)) + "Decimal ID parameter not constructed correctly" + } + test "succeeds for single ID" { + Expect.equal + (idParam (single 5.67)) + ("@id", Sql.double (double (single 5.67))) + "Single ID parameter not constructed correctly" + } + test "succeeds for double ID" { + Expect.equal + (idParam (double 6.78)) + ("@id", Sql.double (double 6.78)) + "Double ID parameter not constructed correctly" + } + test "succeeds for string ID" { + Expect.equal (idParam "99") ("@id", Sql.string "99") "String ID parameter not constructed correctly" + } + test "succeeds for non-numeric non-string ID" { + let target = { new obj() with override _.ToString() = "ToString was called" } + Expect.equal + (idParam target) + ("@id", Sql.string "ToString was called") + "Non-numeric, non-string parameter not constructed correctly" + } ] + test "jsonParam succeeds" { + Expect.equal + (jsonParam "@test" {| Something = "good" |}) + ("@test", Sql.jsonb """{"Something":"good"}""") + "JSON parameter not constructed correctly" + } + testList "addFieldParams" [ + test "succeeds when a parameter is added" { + let paramList = addFieldParams [ Field.EQ "it" "242" ] [] + Expect.hasLength paramList 1 "There should have been a parameter added" + let name, value = Seq.head paramList + Expect.equal name "@field0" "Field parameter name not correct" + Expect.equal value (Sql.string "242") "Parameter value not correct" + } + test "succeeds when multiple independent parameters are added" { + let paramList = addFieldParams [ Field.EQ "me" "you"; Field.GT "us" "them" ] [ idParam 14 ] + Expect.hasLength paramList 3 "There should have been 2 parameters added" + let p = Array.ofSeq paramList + Expect.equal (fst p[0]) "@id" "First field parameter name not correct" + Expect.equal (snd p[0]) (Sql.int 14) "First parameter value not correct" + Expect.equal (fst p[1]) "@field0" "Second field parameter name not correct" + Expect.equal (snd p[1]) (Sql.string "you") "Second parameter value not correct" + Expect.equal (fst p[2]) "@field1" "Third parameter name not correct" + Expect.equal (snd p[2]) (Sql.string "them") "Third parameter value not correct" + } + test "succeeds when a parameter is not added" { + let paramList = addFieldParams [ Field.EX "tacos" ] [] + Expect.isEmpty paramList "There should not have been any parameters added" + } + test "succeeds when two parameters are added for one field" { + let paramList = addFieldParams [ { Field.BT "that" "eh" "zed" with ParameterName = Some "@test" } ] [] + Expect.hasLength paramList 2 "There should have been 2 parameters added" + let name, value = Seq.head paramList + Expect.equal name "@testmin" "Minimum field name not correct" + Expect.equal value (Sql.string "eh") "Minimum parameter value not correct" + let name, value = paramList |> Seq.skip 1 |> Seq.head + Expect.equal name "@testmax" "Maximum field name not correct" + Expect.equal value (Sql.string "zed") "Maximum parameter value not correct" + } + ] + testList "fieldNameParams" [ + test "succeeds for one name" { + let name, value = fieldNameParams [ "bob" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.string "bob") "The parameter value was incorrect" + } + test "succeeds for multiple names" { + let name, value = fieldNameParams [ "bob"; "tom"; "mike" ] + Expect.equal name "@name" "The parameter name was incorrect" + Expect.equal value (Sql.stringArray [| "bob"; "tom"; "mike" |]) "The parameter value was incorrect" + } + ] + test "noParams succeeds" { + Expect.isEmpty noParams "The no-params sequence should be empty" + } +] + +/// Unit tests for the Query module of the PostgreSQL library +let queryTests = testList "Query" [ + testList "whereByFields" [ + test "succeeds for a single field when a logical operator is passed" { + Expect.equal + (Query.whereByFields Any [ { Field.GT "theField" "0" with ParameterName = Some "@test" } ]) + "data->>'theField' > @test" + "WHERE clause not correct" + } + test "succeeds for a single field when an existence operator is passed" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField" ]) + "data->>'thatField' IS NULL" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed with numeric values" { + Expect.equal + (Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ]) + "(data->>'aField')::numeric BETWEEN @rangemin AND @rangemax" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed with non-numeric values" { + Expect.equal + (Query.whereByFields Any [ { Field.BT "field0" "a" "b" with ParameterName = Some "@alpha" } ]) + "data->>'field0' BETWEEN @alphamin AND @alphamax" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with logical operators" { + Expect.equal + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" + "WHERE clause not correct" + } + test "succeeds for any multiple fields with an existence operator" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) + "data->>'thatField' IS NULL OR (data->>'thisField')::numeric >= @field0" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with between operators" { + Expect.equal + (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) + "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" + } + ] + testList "whereById" [ + test "succeeds for numeric ID" { + Expect.equal (Query.whereById 18) "(data->>'Id')::numeric = @id" "WHERE clause not correct" + } + test "succeeds for string ID" { + Expect.equal (Query.whereById "18") "data->>'Id' = @id" "WHERE clause not correct" + } + test "succeeds for non-numeric non-string ID" { + Expect.equal + (Query.whereById (System.Uri "https://example.com")) "data->>'Id' = @id" "WHERE clause not correct" + } + ] + testList "Definition" [ + test "ensureTable succeeds" { + Expect.equal + (Query.Definition.ensureTable PostgresDb.TableName) + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" + "CREATE TABLE statement not constructed correctly" + } + test "ensureDocumentIndex succeeds for full index" { + Expect.equal + (Query.Definition.ensureDocumentIndex "schema.tbl" Full) + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)" + "CREATE INDEX statement not constructed correctly" + } + test "ensureDocumentIndex succeeds for JSONB Path Ops index" { + Expect.equal + (Query.Definition.ensureDocumentIndex PostgresDb.TableName Optimized) + (sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)" + PostgresDb.TableName PostgresDb.TableName) + "CREATE INDEX statement not constructed correctly" + } + ] + test "whereDataContains succeeds" { + Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" + } + test "whereJsonPathMatches succeeds" { + Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" + } + test "patch succeeds" { + Expect.equal + (Query.patch PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data - @name" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE (data->>'That')::numeric > @field0" + "By-Field query not correct" + } + test "byContains succeeds" { + Expect.equal (Query.byContains "exam") "exam WHERE data @> @criteria" "By-Contains query not correct" + } + test "byPathMach succeeds" { + Expect.equal + (Query.byPathMatch "verify") "verify WHERE data @? @path::jsonpath" "By-JSON Path query not correct" + } +] + +(** INTEGRATION TESTS **) open ThrowawayDb.Postgres open Types -let isTrue<'T> (_ : 'T) = true +/// Load the test documents into the database +let loadDocs () = backgroundTask { + for doc in testDocuments do do! insert PostgresDb.TableName doc +} -let integrationTests = - let documents = [ - { 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 } - ] - let loadDocs () = backgroundTask { - for doc in documents do do! insert PostgresDb.TableName doc +/// Integration tests for the Configuration module of the PostgreSQL library +let configurationTests = testList "Configuration" [ + test "useDataSource disposes existing source" { + use db1 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + let source = PostgresDb.MkDataSource db1.ConnectionString + Configuration.useDataSource source + + use db2 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + Configuration.useDataSource (PostgresDb.MkDataSource db2.ConnectionString) + Expect.throws (fun () -> source.OpenConnection() |> ignore) "Data source should have been disposed" } - testList "Integration" [ - testList "Configuration" [ - test "useDataSource disposes existing source" { - use db1 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - let source = PostgresDb.MkDataSource db1.ConnectionString - Configuration.useDataSource source - - use db2 = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - Configuration.useDataSource (PostgresDb.MkDataSource db2.ConnectionString) - Expect.throws (fun () -> source.OpenConnection() |> ignore) "Data source should have been disposed" - } - test "dataSource returns configured data source" { - use db = ThrowawayDatabase.Create PostgresDb.ConnStr.Value - let source = PostgresDb.MkDataSource db.ConnectionString - Configuration.useDataSource source - - Expect.isTrue (obj.ReferenceEquals(source, Configuration.dataSource ())) - "Data source should have been the same" - } - ] - testList "Custom" [ - testList "list" [ - testTask "succeeds when data is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData - Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" - } - testTask "succeeds when data is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = - Custom.list $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] fromData - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "single" [ - testTask "succeeds when a row is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" - [ "@id", Sql.string "one"] fromData - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when a row is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" - [ "@id", Sql.string "eighty" ] fromData - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "nonQuery" [ - testTask "succeeds when operating on data" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" [] - - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 0 "There should be no documents remaining in the table" - } - testTask "succeeds when no data matches where clause" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" - [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] - - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should be 5 documents remaining in the table" - } - ] - testTask "scalar succeeds" { - use db = PostgresDb.BuildDb() - let! nbr = Custom.scalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value") - Expect.equal nbr 5 "The query should have returned the number 5" - } - ] - testList "Definition" [ - testTask "ensureTable succeeds" { - use db = PostgresDb.BuildDb() - let tableExists () = - Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists - let keyExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists - - let! exists = tableExists () - let! alsoExists = keyExists () - Expect.isFalse exists "The table should not exist already" - Expect.isFalse alsoExists "The key index should not exist already" - - do! Definition.ensureTable "ensured" - let! exists' = tableExists () - let! alsoExists' = keyExists () - Expect.isTrue exists' "The table should now exist" - Expect.isTrue alsoExists' "The key index should now exist" - } - testTask "ensureDocumentIndex succeeds" { - use db = PostgresDb.BuildDb() - let indexExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it" - [] - toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureDocumentIndex "ensured" Optimized - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - testTask "ensureFieldIndex succeeds" { - use db = PostgresDb.BuildDb() - let indexExists () = - Custom.scalar - "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - ] - testList "insert" [ - testTask "succeeds" { - use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } - do! insert PostgresDb.TableName testDoc - let! after = Find.all PostgresDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "fails for duplicate key" { - use db = PostgresDb.BuildDb() - do! insert PostgresDb.TableName { emptyDoc with Id = "test" } - Expect.throws - (fun () -> - insert PostgresDb.TableName {emptyDoc with Id = "test" } - |> Async.AwaitTask - |> Async.RunSynchronously) - "An exception should have been raised for duplicate document ID insert" - } - ] - testList "save" [ - testTask "succeeds when a document is inserted" { - use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! save PostgresDb.TableName testDoc - let! after = Find.all PostgresDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! insert PostgresDb.TableName testDoc - - let! before = Find.byId PostgresDb.TableName "test" - Expect.isSome before "There should have been a document returned" - Expect.equal before.Value testDoc "The document is not correct" - - let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } - do! save PostgresDb.TableName upd8Doc - let! after = Find.byId PostgresDb.TableName "test" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value upd8Doc "The updated document is not correct" - } - ] - testList "Count" [ - testTask "all succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.all PostgresDb.TableName - Expect.equal theCount 5 "There should have been 5 matching documents" - } - testTask "byField succeeds for numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "NumValue" 10 20) - Expect.equal theCount 3 "There should have been 3 matching documents" - } - testTask "byField succeeds for non-numeric range" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField PostgresDb.TableName (Field.BT "Value" "aardvark" "apple") - Expect.equal theCount 1 "There should have been 1 matching document" - } - testTask "byContains succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byContains PostgresDb.TableName {| Value = "purple" |} - Expect.equal theCount 2 "There should have been 2 matching documents" - } - testTask "byJsonPath succeeds" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)" - Expect.equal theCount 3 "There should have been 3 matching documents" - } - ] - testList "Exists" [ - testList "byId" [ - testTask "succeeds when a document exists" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId PostgresDb.TableName "three" - Expect.isTrue exists "There should have been an existing document" - } - testTask "succeeds when a document does not exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId PostgresDb.TableName "seven" - Expect.isFalse exists "There should not have been an existing document" - } - ] - testList "byField" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField PostgresDb.TableName (Field.EX "Sub") - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when documents do not exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField PostgresDb.TableName (Field.EQ "NumValue" "six") - Expect.isFalse exists "There should not have been existing documents" - } - ] - testList "byContains" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byContains PostgresDb.TableName {| NumValue = 10 |} - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byContains PostgresDb.TableName {| Nothing = "none" |} - Expect.isFalse exists "There should not have been any existing documents" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)" - Expect.isFalse exists "There should not have been any existing documents" - } - ] - ] - testList "Find" [ - testList "all" [ - testTask "succeeds when there is data" { - use db = PostgresDb.BuildDb() - - do! insert PostgresDb.TableName { Foo = "one"; Bar = "two" } - do! insert PostgresDb.TableName { Foo = "three"; Bar = "four" } - do! insert PostgresDb.TableName { Foo = "five"; Bar = "six" } - - let! results = Find.all PostgresDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" - } - testTask "succeeds when there is no data" { - use db = PostgresDb.BuildDb() - let! results = Find.all PostgresDb.TableName - Expect.equal results [] "There should have been no documents returned" - } - ] - testList "byId" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId PostgresDb.TableName "two" - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "byField" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField PostgresDb.TableName (Field.EQ "Value" "another") - Expect.equal (List.length docs) 1 "There should have been one document returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField PostgresDb.TableName (Field.EQ "Value" "mauve") - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "byContains" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} - Expect.equal (List.length docs) 2 "There should have been two documents returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byContains PostgresDb.TableName {| Value = "mauve" |} - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 15)" - Expect.equal (List.length docs) 3 "There should have been 3 documents returned" - } - testTask "succeeds when documents are not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "firstByField" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "another") - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "purple") - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField PostgresDb.TableName (Field.EQ "Value" "absent") - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "firstByContains" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Value = "another" |} - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "firstByJsonPath" [ - testTask "succeeds when a document is found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Value ? (@ == "FIRST!")""" - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - Expect.isSome doc "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" - Expect.isNone doc "There should not have been a document returned" - } - ] - ] - testList "Update" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! Update.byId PostgresDb.TableName "one" testDoc - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value testDoc "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byId - PostgresDb.TableName - "test" - { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } - } - ] - testList "byFunc" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Update.byFunc PostgresDb.TableName (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal - after.Value - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byFunc - PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - } - ] - ] - testList "Patch" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byId PostgresDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId PostgresDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value.NumValue 44 "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} - } - ] - testList "byField" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byField PostgresDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = Count.byField PostgresDb.TableName (Field.EQ "NumValue" "77") - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byField PostgresDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} - } - ] - testList "byContains" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} - let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |} - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} - } - ] - testList "byJsonPath" [ - testTask "succeeds when a document is updated" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} - let! after = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)" - Expect.equal after 2 "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use db = PostgresDb.BuildDb() - - let! before = Count.all PostgresDb.TableName - Expect.equal before 0 "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} - } - ] - ] - testList "RemoveFields" [ - testList "byId" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byId PostgresDb.TableName "two" [ "AFieldThatIsNotThere" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byId PostgresDb.TableName "two" [ "Value" ] - } - ] - testList "byField" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byField PostgresDb.TableName (Field.EQ "NumValue" "17") [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byField PostgresDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] - } - ] - testList "byContains" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byContains PostgresDb.TableName {| Abracadabra = "apple" |} [ "Value" ] - } - ] - testList "byJsonPath" [ - testTask "succeeds when multiple fields are removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 1 "There should be 1 document without Value fields" - } - testTask "succeeds when a single field is removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] - let! noSubs = Count.byField PostgresDb.TableName (Field.NEX "Sub") - Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" - let! noValue = Count.byField PostgresDb.TableName (Field.NEX "Value") - Expect.equal noValue 0 "There should be no documents without Value fields" - } - testTask "succeeds when a field is not removed" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use db = PostgresDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byJsonPath PostgresDb.TableName "$.Abracadabra ? (@ == \"apple\")" [ "Value" ] - } - ] - ] - testList "Delete" [ - testList "byId" [ - testTask "succeeds when a document is deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byId PostgresDb.TableName "four" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 4 "There should have been 4 documents remaining" - } - testTask "succeeds when a document is not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byId PostgresDb.TableName "thirty" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byField" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byField PostgresDb.TableName (Field.EQ "Value" "purple") - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byField PostgresDb.TableName (Field.EQ "Value" "crimson") - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byContains" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byContains PostgresDb.TableName {| Value = "purple" |} - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byContains PostgresDb.TableName {| Value = "crimson" |} - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - testList "byJsonPath" [ - testTask "succeeds when documents are deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 3 "There should have been 3 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use db = PostgresDb.BuildDb() - do! loadDocs () - - do! Delete.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)" - let! remaining = Count.all PostgresDb.TableName - Expect.equal remaining 5 "There should have been 5 documents remaining" - } - ] - ] + test "dataSource returns configured data source" { + use db = ThrowawayDatabase.Create PostgresDb.ConnStr.Value + let source = PostgresDb.MkDataSource db.ConnectionString + Configuration.useDataSource source + + Expect.isTrue (obj.ReferenceEquals(source, Configuration.dataSource ())) "Data source should have been the same" + } +] + +/// Integration tests for the Custom module of the PostgreSQL library +let customTests = testList "Custom" [ + testList "list" [ + testTask "succeeds when data is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Custom.list (Query.find PostgresDb.TableName) [] fromData + Expect.hasLength docs 5 "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Custom.list + $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" + [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } ] - |> testSequenced + testList "single" [ + testTask "succeeds when a row is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + let! doc = + Custom.single + $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" + [ "@id", Sql.string "one"] + fromData + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when a row is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () -let all = testList "Postgres" [ unitTests; integrationTests ] + let! doc = + Custom.single + $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" + [ "@id", Sql.string "eighty" ] + fromData + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "nonQuery" [ + testTask "succeeds when operating on data" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" [] + + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 0 "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery + $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" + [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] + + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use db = PostgresDb.BuildDb() + let! nbr = Custom.scalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value") + Expect.equal nbr 5 "The query should have returned the number 5" + } +] + +/// Integration tests for the Definition module of the PostgreSQL library +let definitionTests = testList "Definition" [ + testTask "ensureTable succeeds" { + use db = PostgresDb.BuildDb() + let tableExists () = + Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists + let keyExists () = + Custom.scalar + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists + + let! exists = tableExists () + let! alsoExists = keyExists () + Expect.isFalse exists "The table should not exist already" + Expect.isFalse alsoExists "The key index should not exist already" + + do! Definition.ensureTable "ensured" + let! exists' = tableExists () + let! alsoExists' = keyExists () + Expect.isTrue exists' "The table should now exist" + Expect.isTrue alsoExists' "The key index should now exist" + } + testTask "ensureDocumentIndex succeeds" { + use db = PostgresDb.BuildDb() + let indexExists () = + Custom.scalar + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it" [] toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureDocumentIndex "ensured" Optimized + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } + testTask "ensureFieldIndex succeeds" { + use db = PostgresDb.BuildDb() + let indexExists () = + Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } +] + +/// Integration tests for the (auto-opened) Document module of the PostgreSQL library +let documentTests = testList "Document" [ + testList "insert" [ + testTask "succeeds" { + use db = PostgresDb.BuildDb() + let! before = Find.all PostgresDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } + do! insert PostgresDb.TableName testDoc + let! after = Find.all PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use db = PostgresDb.BuildDb() + do! insert PostgresDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + insert PostgresDb.TableName {emptyDoc with Id = "test" } + |> Async.AwaitTask + |> Async.RunSynchronously) + "An exception should have been raised for duplicate document ID insert" + } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { Key = 0; Text = "one" } + do! insert PostgresDb.TableName { Key = 0; Text = "two" } + do! insert PostgresDb.TableName { Key = 77; Text = "three" } + do! insert PostgresDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered PostgresDb.TableName [ Field.Named "n:Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } + ] + testList "save" [ + testTask "succeeds when a document is inserted" { + use db = PostgresDb.BuildDb() + let! before = Find.all PostgresDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! save PostgresDb.TableName testDoc + let! after = Find.all PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! insert PostgresDb.TableName testDoc + + let! before = Find.byId PostgresDb.TableName "test" + Expect.isSome before "There should have been a document returned" + Expect.equal before.Value testDoc "The document is not correct" + + let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } + do! save PostgresDb.TableName upd8Doc + let! after = Find.byId PostgresDb.TableName "test" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] +] + +/// Integration tests for the Count module of the PostgreSQL library +let countTests = testList "Count" [ + testTask "all succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.all PostgresDb.TableName + Expect.equal theCount 5 "There should have been 5 matching documents" + } + testList "byFields" [ + testTask "succeeds when items are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields PostgresDb.TableName Any [ Field.BT "NumValue" 15 20; Field.EQ "NumValue" 0 ] + Expect.equal theCount 3 "There should have been 3 matching documents" + } + testTask "succeeds when items are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields PostgresDb.TableName All [ Field.EX "Sub"; Field.GT "NumValue" 100 ] + Expect.equal theCount 0 "There should have been no matching documents" + } + ] + testTask "byContains succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byContains PostgresDb.TableName {| Value = "purple" |} + Expect.equal theCount 2 "There should have been 2 matching documents" + } + testTask "byJsonPath succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)" + Expect.equal theCount 3 "There should have been 3 matching documents" + } +] + +/// Integration tests for the Exists module of the PostgreSQL library +let existsTests = testList "Exists" [ + testList "byId" [ + testTask "succeeds when a document exists" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId PostgresDb.TableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId PostgresDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "byFields" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields PostgresDb.TableName Any [ Field.EX "Sub"; Field.EX "Boo" ] + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when documents do not exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields PostgresDb.TableName All [ Field.EQ "NumValue" "six"; Field.EX "Nope" ] + Expect.isFalse exists "There should not have been existing documents" + } + ] + testList "byContains" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byContains PostgresDb.TableName {| NumValue = 10 |} + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byContains PostgresDb.TableName {| Nothing = "none" |} + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)" + Expect.isFalse exists "There should not have been any existing documents" + } + ] +] + +/// Integration tests for the Find module of the PostgreSQL library +let findTests = testList "Find" [ + testList "all" [ + testTask "succeeds when there is data" { + use db = PostgresDb.BuildDb() + + do! insert PostgresDb.TableName { Foo = "one"; Bar = "two" } + do! insert PostgresDb.TableName { Foo = "three"; Bar = "four" } + do! insert PostgresDb.TableName { Foo = "five"; Bar = "six" } + + let! results = Find.all PostgresDb.TableName + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" + } + testTask "succeeds when there is no data" { + use db = PostgresDb.BuildDb() + let! results = Find.all PostgresDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "allOrdered" [ + testTask "succeeds when ordering numerically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered PostgresDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] + testList "byId" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId PostgresDb.TableName "two" + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "byFields" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields PostgresDb.TableName All [ Field.EQ "Value" "purple"; Field.EX "Sub" ] + Expect.equal (List.length docs) 1 "There should have been one document returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFields + PostgresDb.TableName All [ Field.EQ "Value" "mauve"; Field.NE "NumValue" 40 ] + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + PostgresDb.TableName All [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "Documents not ordered correctly" + } + ] + testList "byContains" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byContains PostgresDb.TableName {| Value = "mauve" |} + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byContainsOrdered" [ + // Id = two, Sub.Bar = blue; Id = four, Sub.Bar = red + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|four" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Sub.Bar DESC" ] + Expect.hasLength docs 2 "There should have been two documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|two" "Documents not ordered correctly" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 15)" + Expect.equal (List.length docs) 3 "There should have been 3 documents returned" + } + testTask "succeeds when documents are not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "byJsonPathOrdered" [ + // Id = one, NumValue = 0; Id = two, NumValue = 10; Id = three, NumValue = 4 + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "one|three|two" "Documents not ordered correctly" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byJsonPathOrdered + PostgresDb.TableName "$.NumValue ? (@ < 15)" [ Field.Named "n:NumValue DESC" ] + Expect.hasLength docs 3 "There should have been 3 documents returned" + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "two|three|one" "Documents not ordered correctly" + } + ] + testList "firstByFields" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "five"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields PostgresDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "five" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] [ Field.Named "Id DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] + testList "firstByContains" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Value = "another" |} + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByContainsOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByContainsOrdered + PostgresDb.TableName {| Sub = {| Foo = "green" |} |} [ Field.Named "Value DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] + testList "firstByJsonPath" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Value ? (@ == "FIRST!")""" + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByJsonPathOrdered" [ + testTask "succeeds when sorting ascending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByJsonPathOrdered + PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] +] + +/// Integration tests for the Update module of the PostgreSQL library +let updateTests = testList "Update" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! Update.byId PostgresDb.TableName "one" testDoc + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value testDoc "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byId + PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "byFunc" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.byFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal + after.Value + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] +] + +/// Integration tests for the Patch module of the PostgreSQL library +let patchTests = testList "Patch" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byId PostgresDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value.NumValue 44 "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} + } + ] + testList "byFields" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" 77 ] + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byFields PostgresDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} + } + ] + testList "byContains" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} + let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |} + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + } + ] + testList "byJsonPath" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} + let! after = Count.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)" + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} + } + ] +] + +/// Integration tests for the RemoveFields module of the PostgreSQL library +let removeFieldsTests = testList "RemoveFields" [ + testList "byId" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId PostgresDb.TableName "two" [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byId PostgresDb.TableName "two" [ "AFieldThatIsNotThere" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byId PostgresDb.TableName "two" [ "Value" ] + } + ] + testList "byFields" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.EQ "NumValue" "17" ] [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byFields PostgresDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] + } + ] + testList "byContains" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byContains PostgresDb.TableName {| NumValue = 17 |} [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byContains PostgresDb.TableName {| Abracadabra = "apple" |} [ "Value" ] + } + ] + testList "byJsonPath" [ + testTask "succeeds when multiple fields are removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub"; "Value" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 1 "There should be 1 document without Value fields" + } + testTask "succeeds when a single field is removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Sub" ] + let! noSubs = Count.byFields PostgresDb.TableName Any [ Field.NEX "Sub" ] + Expect.equal noSubs 4 "There should now be 4 documents without Sub fields" + let! noValue = Count.byFields PostgresDb.TableName Any [ Field.NEX "Value" ] + Expect.equal noValue 0 "There should be no documents without Value fields" + } + testTask "succeeds when a field is not removed" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byJsonPath PostgresDb.TableName "$.NumValue ? (@ == 17)" [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use db = PostgresDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byJsonPath PostgresDb.TableName "$.Abracadabra ? (@ == \"apple\")" [ "Value" ] + } + ] +] + +/// Integration tests for the Delete module of the PostgreSQL library +let deleteTests = testList "Delete" [ + testList "byId" [ + testTask "succeeds when a document is deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byId PostgresDb.TableName "four" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 4 "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byId PostgresDb.TableName "thirty" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byFields" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "purple" ] + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byFields PostgresDb.TableName Any [ Field.EQ "Value" "crimson" ] + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byContains" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byContains PostgresDb.TableName {| Value = "purple" |} + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byContains PostgresDb.TableName {| Value = "crimson" |} + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "byJsonPath" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Delete.byJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)" + let! remaining = Count.all PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] +] + +/// All tests for the PostgreSQL library +let all = testList "Postgres" [ + testList "Unit" [ parametersTests; queryTests ] + testSequenced <| testList "Integration" [ + configurationTests + customTests + definitionTests + documentTests + countTests + existsTests + findTests + updateTests + patchTests + removeFieldsTests + deleteTests + ] +] diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index d8a43ff..51d4f39 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -113,12 +113,12 @@ let integrationTests = let! theCount = conn.countAll SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } - testTask "countByField succeeds" { + testTask "countByFields succeeds" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! theCount = conn.countByField SqliteDb.TableName (Field.EQ "Value" "purple") + let! theCount = conn.countByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] Expect.equal theCount 2L "There should have been 2 matching documents" } testList "existsById" [ @@ -139,13 +139,13 @@ let integrationTests = Expect.isFalse exists "There should not have been an existing document" } ] - testList "existsByField" [ + testList "existsByFields" [ testTask "succeeds when documents exist" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "NumValue" 10) + let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when no matching documents exist" { @@ -153,7 +153,7 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField SqliteDb.TableName (Field.EQ "Nothing" "none") + let! exists = conn.existsByFields SqliteDb.TableName Any [ Field.EQ "Nothing" "none" ] Expect.isFalse exists "There should not have been any existing documents" } ] @@ -181,6 +181,44 @@ let integrationTests = Expect.equal results [] "There should have been no documents returned" } ] + testList "findAllOrdered" [ + testTask "succeeds when ordering numerically" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! results = conn.findAllOrdered SqliteDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] testList "findById" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() @@ -188,7 +226,7 @@ let integrationTests = do! loadDocs () let! doc = conn.findById SqliteDb.TableName "two" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -197,35 +235,59 @@ let integrationTests = do! loadDocs () let! doc = conn.findById SqliteDb.TableName "three hundred eighty-seven" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + Expect.isNone doc "There should not have been a document returned" } ] - testList "findByField" [ + testList "findByFields" [ testTask "succeeds when documents are found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") - Expect.equal (List.length docs) 2 "There should have been two documents returned" + let! docs = conn.findByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.hasLength docs 2 "There should have been two documents returned" } testTask "succeeds when documents are not found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField SqliteDb.TableName (Field.EQ "Value" "mauve") - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + let! docs = conn.findByFields SqliteDb.TableName Any [ Field.EQ "Value" "mauve" ] + Expect.isEmpty docs "There should have been no documents returned" } ] - testList "findFirstByField" [ + testList "findByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.findByFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.findByFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly" + } + ] + testList "findFirstByFields" [ testTask "succeeds when a document is found" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Value" "another") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isSome doc "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } testTask "succeeds when multiple documents are found" { @@ -233,8 +295,8 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.isSome doc "There should have been a document returned" Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" } testTask "succeeds when a document is not found" { @@ -242,8 +304,32 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField SqliteDb.TableName (Field.EQ "Value" "absent") - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + let! doc = conn.findFirstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "findFirstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.findFirstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.findFirstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" } ] testList "updateById" [ @@ -324,14 +410,14 @@ let integrationTests = do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |} } ] - testList "patchByField" [ + testList "patchByFields" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = conn.countByField SqliteDb.TableName (Field.EQ "NumValue" 77) + do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = conn.countByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] Expect.equal after 2L "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -342,7 +428,7 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.patchByField SqliteDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} + do! conn.patchByFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} } ] testList "removeFieldsById" [ @@ -375,13 +461,13 @@ let integrationTests = do! conn.removeFieldsById SqliteDb.TableName "two" [ "Value" ] } ] - testList "removeFieldByField" [ + testList "removeFieldByFields" [ testTask "succeeds when a field is removed" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Sub" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] try let! _ = conn.findById SqliteDb.TableName "four" Expect.isTrue false "The updated document should have failed to parse" @@ -395,14 +481,14 @@ let integrationTests = do! loadDocs () // This not raising an exception is the test - do! conn.removeFieldsByField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Nothing" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] } testTask "succeeds when no document is matched" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () // This not raising an exception is the test - do! conn.removeFieldsByField SqliteDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] + do! conn.removeFieldsByFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] } ] testList "deleteById" [ @@ -425,13 +511,13 @@ let integrationTests = Expect.equal remaining 5L "There should have been 5 documents remaining" } ] - testList "deleteByField" [ + testList "deleteByFields" [ testTask "succeeds when documents are deleted" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField SqliteDb.TableName (Field.NE "Value" "purple") + do! conn.deleteByFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 2L "There should have been 2 documents remaining" } @@ -440,7 +526,7 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField SqliteDb.TableName (Field.EQ "Value" "crimson") + do! conn.deleteByFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } @@ -478,8 +564,8 @@ let integrationTests = use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData - Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" + let! docs = conn.customList (Query.find SqliteDb.TableName) [] fromData + Expect.hasLength docs 5 "There should have been 5 documents returned" } testTask "succeeds when data is not found" { use! db = SqliteDb.BuildDb() diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 6d97893..ebf0293 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -8,700 +8,793 @@ open Expecto open Microsoft.Data.Sqlite open Types -/// Unit tests for the SQLite library -let unitTests = - testList "Unit" [ - testList "Query" [ - testList "whereByField" [ - test "succeeds when a logical operator is passed" { - Expect.equal - (Query.whereByField (Field.GT "theField" 0) "@test") - "data->>'theField' > @test" - "WHERE clause not correct" - } - test "succeeds when an existence operator is passed" { - Expect.equal - (Query.whereByField (Field.NEX "thatField") "") - "data->>'thatField' IS NULL" - "WHERE clause not correct" - } - test "succeeds when the between operator is passed" { - Expect.equal - (Query.whereByField (Field.BT "aField" 50 99) "@range") - "data->>'aField' BETWEEN @rangemin AND @rangemax" - "WHERE clause not correct" - } - ] - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" - } - test "Definition.ensureTable succeeds" { - Expect.equal - (Query.Definition.ensureTable "tbl") - "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" - "CREATE TABLE statement not correct" - } - test "update succeeds" { - Expect.equal - (Query.update "tbl") - "UPDATE tbl SET data = @data WHERE data->>'Id' = @id" - "UPDATE full statement not correct" - } - testList "Count" [ - test "all succeeds" { - Expect.equal (Query.Count.all "tbl") $"SELECT COUNT(*) AS it FROM tbl" "Count query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Count.byField "tbl" (Field.EQ "thatField" 0)) - "SELECT COUNT(*) AS it FROM tbl WHERE data->>'thatField' = @field" - "JSON field text comparison count query not correct" - } - ] - testList "Exists" [ - test "byId succeeds" { - Expect.equal - (Query.Exists.byId "tbl") - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Id' = @id) AS it" - "ID existence query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Exists.byField "tbl" (Field.LT "Test" 0)) - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data->>'Test' < @field) AS it" - "JSON field text comparison exists query not correct" - } - ] - testList "Find" [ - test "byId succeeds" { - Expect.equal - (Query.Find.byId "tbl") - "SELECT data FROM tbl WHERE data->>'Id' = @id" - "SELECT by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Find.byField "tbl" (Field.GE "Golf" 0)) - "SELECT data FROM tbl WHERE data->>'Golf' >= @field" - "SELECT by JSON comparison query not correct" - } - ] - testList "Patch" [ - test "byId succeeds" { - Expect.equal - (Query.Patch.byId "tbl") - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Patch.byField "tbl" (Field.NE "Part" 0)) - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data->>'Part' <> @field" - "UPDATE partial by JSON comparison query not correct" - } - ] - testList "RemoveFields" [ - test "byId succeeds" { - Expect.equal - (Query.RemoveFields.byId "tbl" [ SqliteParameter("@name", "one") ]) - "UPDATE tbl SET data = json_remove(data, @name) WHERE data->>'Id' = @id" - "Remove field by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.RemoveFields.byField - "tbl" - (Field.GT "Fly" 0) - [ SqliteParameter("@name0", "one"); SqliteParameter("@name1", "two") ]) - "UPDATE tbl SET data = json_remove(data, @name0, @name1) WHERE data->>'Fly' > @field" - "Remove field by field query not correct" - } - ] - testList "Delete" [ - test "byId succeeds" { - Expect.equal - (Query.Delete.byId "tbl") - "DELETE FROM tbl WHERE data->>'Id' = @id" - "DELETE by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Delete.byField "tbl" (Field.NEX "gone")) - "DELETE FROM tbl WHERE data->>'gone' IS NULL" - "DELETE by JSON comparison query not correct" - } - ] - ] - testList "Parameters" [ - test "idParam succeeds" { - let theParam = idParam 7 - Expect.equal theParam.ParameterName "@id" "The parameter name is incorrect" - Expect.equal theParam.Value "7" "The parameter value is incorrect" - } - test "jsonParam succeeds" { - let theParam = jsonParam "@test" {| Nice = "job" |} - Expect.equal theParam.ParameterName "@test" "The parameter name is incorrect" - Expect.equal theParam.Value """{"Nice":"job"}""" "The parameter value is incorrect" - } - testList "addFieldParam" [ - test "succeeds when adding a parameter" { - let paramList = addFieldParam "@field" (Field.EQ "it" 99) [] - Expect.hasLength paramList 1 "There should have been a parameter added" - let theParam = paramList[0] - Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" - Expect.equal theParam.Value 99 "The parameter value is incorrect" - } - test "succeeds when not adding a parameter" { - let paramList = addFieldParam "@it" (Field.NEX "Coffee") [] - Expect.isEmpty paramList "There should not have been any parameters added" - } - ] - test "noParams succeeds" { - Expect.isEmpty noParams "The parameter list should have been empty" - } - ] - // Results are exhaustively executed in the context of other tests - ] +#nowarn "0044" -/// These tests each use a fresh copy of a SQLite database -let integrationTests = - let documents = [ - { 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 } - ] - let loadDocs () = backgroundTask { - for doc in documents do do! insert SqliteDb.TableName doc - } - testList "Integration" [ - testList "Configuration" [ - test "useConnectionString / connectionString succeed" { - try - Configuration.useConnectionString "Data Source=test.db" - Expect.equal - Configuration.connectionString - (Some "Data Source=test.db;Foreign Keys=True") - "Connection string incorrect" - finally - Configuration.useConnectionString "Data Source=:memory:" - } - 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 """{"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" { - 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" - Configuration.useIdField "Id" - } - ] - testList "Custom" [ - testList "single" [ - testTask "succeeds when a row is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" - [ SqliteParameter("@id", "one") ] - fromData - Expect.isSome doc "There should have been a document returned" - Expect.equal doc.Value.Id "one" "The incorrect document was returned" - } - testTask "succeeds when a row is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = - Custom.single - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" - [ SqliteParameter("@id", "eighty") ] - fromData - Expect.isNone doc "There should not have been a document returned" - } - ] - testList "list" [ - testTask "succeeds when data is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Custom.list (Query.selectFromTable SqliteDb.TableName) [] fromData - Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" - } - testTask "succeeds when data is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = - Custom.list - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - fromData - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "nonQuery" [ - testTask "succeeds when operating on data" { - use! db = SqliteDb.BuildDb() - do! loadDocs () +(** UNIT TESTS **) - do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" [] - - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 0L "There should be no documents remaining in the table" - } - testTask "succeeds when no data matches where clause" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Custom.nonQuery - $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should be 5 documents remaining in the table" - } - ] - testTask "scalar succeeds" { - use! db = SqliteDb.BuildDb() - - let! nbr = Custom.scalar "SELECT 5 AS test_value" [] _.GetInt32(0) - Expect.equal nbr 5 "The query should have returned the number 5" - } - ] - testList "Definition" [ - testTask "ensureTable succeeds" { - use! db = SqliteDb.BuildDb() - let itExists (name: string) = - Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" - [ SqliteParameter("@name", name) ] - toExists - - let! exists = itExists "ensured" - let! alsoExists = itExists "idx_ensured_key" - Expect.isFalse exists "The table should not exist already" - Expect.isFalse alsoExists "The key index should not exist already" - - do! Definition.ensureTable "ensured" - let! exists' = itExists "ensured" - let! alsoExists' = itExists "idx_ensured_key" - Expect.isTrue exists' "The table should now exist" - Expect.isTrue alsoExists' "The key index should now exist" - } - testTask "ensureFieldIndex succeeds" { - use! db = SqliteDb.BuildDb() - let indexExists () = - Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it" - [] - toExists - - let! exists = indexExists () - Expect.isFalse exists "The index should not exist already" - - do! Definition.ensureTable "ensured" - do! Definition.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ] - let! exists' = indexExists () - Expect.isTrue exists' "The index should now exist" - } - ] - testList "insert" [ - testTask "succeeds" { - use! db = SqliteDb.BuildDb() - let! before = Find.all SqliteDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } - do! insert SqliteDb.TableName testDoc - let! after = Find.all SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "fails for duplicate key" { - use! db = SqliteDb.BuildDb() - do! insert SqliteDb.TableName { emptyDoc with Id = "test" } - Expect.throws - (fun () -> - insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) - "An exception should have been raised for duplicate document ID insert" - } - ] - testList "save" [ - testTask "succeeds when a document is inserted" { - use! db = SqliteDb.BuildDb() - let! before = Find.all SqliteDb.TableName - Expect.equal before [] "There should be no documents in the table" - - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! save SqliteDb.TableName testDoc - let! after = Find.all SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! insert SqliteDb.TableName testDoc - - let! before = Find.byId SqliteDb.TableName "test" - Expect.isSome before "There should have been a document returned" - Expect.equal before.Value testDoc "The document is not correct" - - let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } - do! save SqliteDb.TableName upd8Doc - let! after = Find.byId SqliteDb.TableName "test" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value upd8Doc "The updated document is not correct" - } - ] - testList "Count" [ - testTask "all succeeds" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.all SqliteDb.TableName - Expect.equal theCount 5L "There should have been 5 matching documents" - } - testTask "byField succeeds for a numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField SqliteDb.TableName (Field.BT "NumValue" 10 20) - Expect.equal theCount 3L "There should have been 3 matching documents" - } - testTask "byField succeeds for a non-numeric range" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! theCount = Count.byField SqliteDb.TableName (Field.BT "Value" "aardvark" "apple") - Expect.equal theCount 1L "There should have been 1 matching document" - } - ] - testList "Exists" [ - testList "byId" [ - testTask "succeeds when a document exists" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId SqliteDb.TableName "three" - Expect.isTrue exists "There should have been an existing document" - } - testTask "succeeds when a document does not exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byId SqliteDb.TableName "seven" - Expect.isFalse exists "There should not have been an existing document" - } - ] - testList "byField" [ - testTask "succeeds when documents exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField SqliteDb.TableName (Field.EQ "NumValue" 10) - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! exists = Exists.byField SqliteDb.TableName (Field.LT "Nothing" "none") - Expect.isFalse exists "There should not have been any existing documents" - } - ] - ] - testList "Find" [ - testList "all" [ - testTask "succeeds when there is data" { - use! db = SqliteDb.BuildDb() - - do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" } - do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" } - do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" } - - let! results = Find.all SqliteDb.TableName - let expected = [ - { Foo = "one"; Bar = "two" } - { Foo = "three"; Bar = "four" } - { Foo = "five"; Bar = "six" } - ] - Expect.equal results expected "There should have been 3 documents returned" - } - testTask "succeeds when there is no data" { - use! db = SqliteDb.BuildDb() - let! results = Find.all SqliteDb.TableName - Expect.equal results [] "There should have been no documents returned" - } - ] - testList "byId" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId SqliteDb.TableName "two" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.byId SqliteDb.TableName "three hundred eighty-seven" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" - } - ] - testList "byField" [ - testTask "succeeds when documents are found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField SqliteDb.TableName (Field.GT "NumValue" 15) - Expect.equal (List.length docs) 2 "There should have been two documents returned" - } - testTask "succeeds when documents are not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! docs = Find.byField SqliteDb.TableName (Field.GT "NumValue" 100) - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" - } - ] - testList "firstByField" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Value" "another") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.equal doc.Value.Id "two" "The incorrect document was returned" - } - testTask "succeeds when multiple documents are found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Sub.Foo" "green") - Expect.isTrue (Option.isSome doc) "There should have been a document returned" - Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" - } - testTask "succeeds when a document is not found" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let! doc = Find.firstByField SqliteDb.TableName (Field.EQ "Value" "absent") - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" - } - ] - ] - testList "Update" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! Update.byId SqliteDb.TableName "one" testDoc - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value testDoc "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byId - SqliteDb.TableName - "test" - { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } - } - ] - testList "byFunc" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Update.byFunc - SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal - after.Value - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Update.byFunc - SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - } - ] - ] - testList "Patch" [ - testList "byId" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Patch.byId SqliteDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId SqliteDb.TableName "one" - Expect.isSome after "There should have been a document returned post-update" - Expect.equal after.Value.NumValue 44 "The updated document is not correct" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} - } - ] - testList "byField" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Patch.byField SqliteDb.TableName (Field.EQ "Value" "purple") {| NumValue = 77 |} - let! after = Count.byField SqliteDb.TableName (Field.EQ "NumValue" 77) - Expect.equal after 2L "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - - let! before = Find.all SqliteDb.TableName - Expect.isEmpty before "There should have been no documents returned" - - // This not raising an exception is the test - do! Patch.byField SqliteDb.TableName (Field.EQ "Value" "burgundy") {| Foo = "green" |} - } - ] - ] - testList "RemoveFields" [ - testList "byId" [ - testTask "succeeds when fields is removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byId SqliteDb.TableName "two" [ "Sub"; "Value" ] - try - let! _ = Find.byId SqliteDb.TableName "two" - Expect.isTrue false "The updated document should have failed to parse" - with - | :? JsonException -> () - | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" - } - testTask "succeeds when a field is not removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byId SqliteDb.TableName "two" [ "AFieldThatIsNotThere" ] - } - testTask "succeeds when no document is matched" { - use! db = SqliteDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byId SqliteDb.TableName "two" [ "Value" ] - } - ] - testList "byField" [ - testTask "succeeds when a field is removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! RemoveFields.byField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Sub" ] - try - let! _ = Find.byId SqliteDb.TableName "four" - Expect.isTrue false "The updated document should have failed to parse" - with - | :? JsonException -> () - | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" - } - testTask "succeeds when a field is not removed" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - // This not raising an exception is the test - do! RemoveFields.byField SqliteDb.TableName (Field.EQ "NumValue" 17) [ "Nothing" ] - } - testTask "succeeds when no document is matched" { - use! db = SqliteDb.BuildDb() - - // This not raising an exception is the test - do! RemoveFields.byField SqliteDb.TableName (Field.NE "Abracadabra" "apple") [ "Value" ] - } - ] - ] - testList "Delete" [ - testList "byId" [ - testTask "succeeds when a document is deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byId SqliteDb.TableName "four" - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 4L "There should have been 4 documents remaining" - } - testTask "succeeds when a document is not deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byId SqliteDb.TableName "thirty" - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - testList "byField" [ - testTask "succeeds when documents are deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byField SqliteDb.TableName (Field.NE "Value" "purple") - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 2L "There should have been 2 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use! db = SqliteDb.BuildDb() - do! loadDocs () - - do! Delete.byField SqliteDb.TableName (Field.EQ "Value" "crimson") - let! remaining = Count.all SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - ] - test "clean up database" { - Configuration.useConnectionString "data source=:memory:" +/// Unit tests for the Query module of the SQLite library +let queryTests = testList "Query" [ + testList "whereByFields" [ + test "succeeds for a single field when a logical operator is passed" { + Expect.equal + (Query.whereByFields Any [ { Field.GT "theField" 0 with ParameterName = Some "@test" } ]) + "data->>'theField' > @test" + "WHERE clause not correct" + } + test "succeeds for a single field when an existence operator is passed" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField" ]) + "data->>'thatField' IS NULL" + "WHERE clause not correct" + } + test "succeeds for a single field when a between operator is passed" { + Expect.equal + (Query.whereByFields All [ { Field.BT "aField" 50 99 with ParameterName = Some "@range" } ]) + "data->>'aField' BETWEEN @rangemin AND @rangemax" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with logical operators" { + Expect.equal + (Query.whereByFields All [ Field.EQ "theFirst" "1"; Field.EQ "numberTwo" "2" ]) + "data->>'theFirst' = @field0 AND data->>'numberTwo' = @field1" + "WHERE clause not correct" + } + test "succeeds for any multiple fields with an existence operator" { + Expect.equal + (Query.whereByFields Any [ Field.NEX "thatField"; Field.GE "thisField" 18 ]) + "data->>'thatField' IS NULL OR data->>'thisField' >= @field0" + "WHERE clause not correct" + } + test "succeeds for all multiple fields with between operators" { + Expect.equal + (Query.whereByFields All [ Field.BT "aField" 50 99; Field.BT "anotherField" "a" "b" ]) + "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" + "WHERE clause not correct" } ] - |> testSequenced + test "whereById succeeds" { + Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct" + } + test "patch succeeds" { + Expect.equal + (Query.patch SqliteDb.TableName) + $"UPDATE {SqliteDb.TableName} SET data = json_patch(data, json(@data))" + "Patch query not correct" + } + test "removeFields succeeds" { + Expect.equal + (Query.removeFields SqliteDb.TableName [ SqliteParameter("@a", "a"); SqliteParameter("@b", "b") ]) + $"UPDATE {SqliteDb.TableName} SET data = json_remove(data, @a, @b)" + "Field removal query not correct" + } + test "byId succeeds" { + Expect.equal (Query.byId "test" "14") "test WHERE data->>'Id' = @id" "By-ID query not correct" + } + test "byFields succeeds" { + Expect.equal + (Query.byFields "unit" Any [ Field.GT "That" 14 ]) + "unit WHERE data->>'That' > @field0" + "By-Field query not correct" + } + test "Definition.ensureTable succeeds" { + Expect.equal + (Query.Definition.ensureTable "tbl") + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" + "CREATE TABLE statement not correct" + } +] -let all = testList "Sqlite" [ unitTests; integrationTests ] +/// Unit tests for the Parameters module of the SQLite library +let parametersTests = testList "Parameters" [ + test "idParam succeeds" { + let theParam = idParam 7 + Expect.equal theParam.ParameterName "@id" "The parameter name is incorrect" + Expect.equal theParam.Value "7" "The parameter value is incorrect" + } + test "jsonParam succeeds" { + let theParam = jsonParam "@test" {| Nice = "job" |} + Expect.equal theParam.ParameterName "@test" "The parameter name is incorrect" + Expect.equal theParam.Value """{"Nice":"job"}""" "The parameter value is incorrect" + } + testList "addFieldParam" [ + test "succeeds when adding a parameter" { + let paramList = addFieldParam "@field" (Field.EQ "it" 99) [] + Expect.hasLength paramList 1 "There should have been a parameter added" + let theParam = Seq.head paramList + Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" + Expect.equal theParam.Value 99 "The parameter value is incorrect" + } + test "succeeds when not adding a parameter" { + let paramList = addFieldParam "@it" (Field.NEX "Coffee") [] + Expect.isEmpty paramList "There should not have been any parameters added" + } + ] + test "noParams succeeds" { + Expect.isEmpty noParams "The parameter list should have been empty" + } +] +// Results are exhaustively executed in the context of other tests + + +(** INTEGRATION TESTS **) + +/// Load a table with the test documents +let loadDocs () = backgroundTask { + for doc in testDocuments do do! insert SqliteDb.TableName doc +} + +/// Integration tests for the Configuration module of the SQLite library +let configurationTests = testList "Configuration" [ + test "useConnectionString / connectionString succeed" { + try + Configuration.useConnectionString "Data Source=test.db" + Expect.equal + Configuration.connectionString + (Some "Data Source=test.db;Foreign Keys=True") + "Connection string incorrect" + finally + Configuration.useConnectionString "Data Source=:memory:" + } +] + +/// Integration tests for the Custom module of the SQLite library +let customTests = testList "Custom" [ + testList "single" [ + testTask "succeeds when a row is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" + [ SqliteParameter("@id", "one") ] + fromData + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "one" "The incorrect document was returned" + } + testTask "succeeds when a row is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" + [ SqliteParameter("@id", "eighty") ] + fromData + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "list" [ + testTask "succeeds when data is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Custom.list (Query.find SqliteDb.TableName) [] fromData + Expect.hasCountOf docs 5u (fun _ -> true) "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Custom.list + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "nonQuery" [ + testTask "succeeds when operating on data" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" [] + + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 0L "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Custom.nonQuery + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use! db = SqliteDb.BuildDb() + + let! nbr = Custom.scalar "SELECT 5 AS test_value" [] _.GetInt32(0) + Expect.equal nbr 5 "The query should have returned the number 5" + } +] + +/// Integration tests for the Definition module of the SQLite library +let definitionTests = testList "Definition" [ + testTask "ensureTable succeeds" { + use! db = SqliteDb.BuildDb() + let itExists (name: string) = + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" + [ SqliteParameter("@name", name) ] + toExists + + let! exists = itExists "ensured" + let! alsoExists = itExists "idx_ensured_key" + Expect.isFalse exists "The table should not exist already" + Expect.isFalse alsoExists "The key index should not exist already" + + do! Definition.ensureTable "ensured" + let! exists' = itExists "ensured" + let! alsoExists' = itExists "idx_ensured_key" + Expect.isTrue exists' "The table should now exist" + Expect.isTrue alsoExists' "The key index should now exist" + } + testTask "ensureFieldIndex succeeds" { + use! db = SqliteDb.BuildDb() + let indexExists () = + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = 'idx_ensured_test') AS it" + [] + toExists + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } +] + +/// Integration tests for the (auto-opened) Document module of the SQLite library +let documentTests = testList "Document" [ + testList "insert" [ + testTask "succeeds" { + use! db = SqliteDb.BuildDb() + let! before = Find.all SqliteDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } + do! insert SqliteDb.TableName testDoc + let! after = Find.all SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use! db = SqliteDb.BuildDb() + do! insert SqliteDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) + "An exception should have been raised for duplicate document ID insert" + } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { Key = 0; Text = "one" } + do! insert SqliteDb.TableName { Key = 0; Text = "two" } + do! insert SqliteDb.TableName { Key = 77; Text = "three" } + do! insert SqliteDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered SqliteDb.TableName [ Field.Named "Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } + ] + testList "save" [ + testTask "succeeds when a document is inserted" { + use! db = SqliteDb.BuildDb() + let! before = Find.all SqliteDb.TableName + Expect.equal before [] "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! save SqliteDb.TableName testDoc + let! after = Find.all SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! insert SqliteDb.TableName testDoc + + let! before = Find.byId SqliteDb.TableName "test" + Expect.isSome before "There should have been a document returned" + Expect.equal before.Value testDoc "The document is not correct" + + let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } + do! save SqliteDb.TableName upd8Doc + let! after = Find.byId SqliteDb.TableName "test" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] +] + +/// Integration tests for the Count module of the SQLite library +let countTests = testList "Count" [ + testTask "all succeeds" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.all SqliteDb.TableName + Expect.equal theCount 5L "There should have been 5 matching documents" + } + testList "byFields" [ + testTask "succeeds for a numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "NumValue" 10 20 ] + Expect.equal theCount 3L "There should have been 3 matching documents" + } + testTask "succeeds for a non-numeric range" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byFields SqliteDb.TableName Any [ Field.BT "Value" "aardvark" "apple" ] + Expect.equal theCount 1L "There should have been 1 matching document" + } + ] +] + +/// Integration tests for the Exists module of the SQLite library +let existsTests = testList "Exists" [ + testList "byId" [ + testTask "succeeds when a document exists" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId SqliteDb.TableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byId SqliteDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "byFields" [ + testTask "succeeds when documents exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 10 ] + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! exists = Exists.byFields SqliteDb.TableName Any [ Field.LT "Nothing" "none" ] + Expect.isFalse exists "There should not have been any existing documents" + } + ] +] + +/// Integration tests for the Find module of the SQLite library +let findTests = testList "Find" [ + testList "all" [ + testTask "succeeds when there is data" { + use! db = SqliteDb.BuildDb() + + do! insert SqliteDb.TableName { Foo = "one"; Bar = "two" } + do! insert SqliteDb.TableName { Foo = "three"; Bar = "four" } + do! insert SqliteDb.TableName { Foo = "five"; Bar = "six" } + + let! results = Find.all SqliteDb.TableName + Expect.equal + results + [ { Foo = "one"; Bar = "two" }; { Foo = "three"; Bar = "four" }; { Foo = "five"; Bar = "six" } ] + "There should have been 3 documents returned" + } + testTask "succeeds when there is no data" { + use! db = SqliteDb.BuildDb() + let! results = Find.all SqliteDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "allOrdered" [ + testTask "succeeds when ordering numerically" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "n:NumValue" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "one|three|two|four|five" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering numerically descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "n:NumValue DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "five|four|two|three|one" + "The documents were not ordered correctly" + } + testTask "succeeds when ordering alphabetically" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! results = Find.allOrdered SqliteDb.TableName [ Field.Named "Id DESC" ] + Expect.hasLength results 5 "There should have been 5 documents returned" + Expect.equal + (results |> List.map _.Id |> String.concat "|") + "two|three|one|four|five" + "The documents were not ordered correctly" + } + ] + testList "byId" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId SqliteDb.TableName "two" + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId SqliteDb.TableName "three hundred eighty-seven" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "byFields" [ + testTask "succeeds when documents are found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = Find.byFields SqliteDb.TableName Any [ Field.GT "NumValue" 100 ] + Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + } + ] + testList "byFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! docs = + Find.byFieldsOrdered + SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ] + Expect.equal + (docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly" + } + ] + testList "firstByFields" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "another" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal doc.Value.Id "two" "The incorrect document was returned" + } + testTask "succeeds when multiple documents are found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] + Expect.isSome doc "There should have been a document returned" + Expect.contains [ "two"; "four" ] doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when a document is not found" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByFields SqliteDb.TableName Any [ Field.EQ "Value" "absent" ] + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "firstByFieldsOrdered" [ + testTask "succeeds when sorting ascending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "two" doc.Value.Id "An incorrect document was returned" + } + testTask "succeeds when sorting descending" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let! doc = + Find.firstByFieldsOrdered + SqliteDb.TableName Any [ Field.EQ "Sub.Foo" "green" ] [ Field.Named "Sub.Bar DESC" ] + Expect.isSome doc "There should have been a document returned" + Expect.equal "four" doc.Value.Id "An incorrect document was returned" + } + ] +] + +/// Integration tests for the Update module of the SQLite library +let updateTests = testList "Update" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! Update.byId SqliteDb.TableName "one" testDoc + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value testDoc "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byId + SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "byFunc" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Update.byFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal + after.Value + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.byFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] +] + +/// Integration tests for the Patch module of the SQLite library +let patchTests = testList "Patch" [ + testList "byId" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Patch.byId SqliteDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId SqliteDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" + Expect.equal after.Value.NumValue 44 "The updated document is not correct" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} + } + ] + testList "byFields" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "purple" ] {| NumValue = 77 |} + let! after = Count.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 77 ] + Expect.equal after 2L "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + + let! before = Find.all SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! Patch.byFields SqliteDb.TableName Any [ Field.EQ "Value" "burgundy" ] {| Foo = "green" |} + } + ] +] + +/// Integration tests for the RemoveFields module of the SQLite library +let removeFieldsTests = testList "RemoveFields" [ + testList "byId" [ + testTask "succeeds when fields is removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byId SqliteDb.TableName "two" [ "Sub"; "Value" ] + try + let! _ = Find.byId SqliteDb.TableName "two" + Expect.isTrue false "The updated document should have failed to parse" + with + | :? JsonException -> () + | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" + } + testTask "succeeds when a field is not removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byId SqliteDb.TableName "two" [ "AFieldThatIsNotThere" ] + } + testTask "succeeds when no document is matched" { + use! db = SqliteDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byId SqliteDb.TableName "two" [ "Value" ] + } + ] + testList "byFields" [ + testTask "succeeds when a field is removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Sub" ] + try + let! _ = Find.byId SqliteDb.TableName "four" + Expect.isTrue false "The updated document should have failed to parse" + with + | :? JsonException -> () + | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" + } + testTask "succeeds when a field is not removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.EQ "NumValue" 17 ] [ "Nothing" ] + } + testTask "succeeds when no document is matched" { + use! db = SqliteDb.BuildDb() + + // This not raising an exception is the test + do! RemoveFields.byFields SqliteDb.TableName Any [ Field.NE "Abracadabra" "apple" ] [ "Value" ] + } + ] +] + +/// Integration tests for the Delete module of the SQLite library +let deleteTests = testList "Delete" [ + testList "byId" [ + testTask "succeeds when a document is deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byId SqliteDb.TableName "four" + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 4L "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byId SqliteDb.TableName "thirty" + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "byFields" [ + testTask "succeeds when documents are deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byFields SqliteDb.TableName Any [ Field.NE "Value" "purple" ] + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 2L "There should have been 2 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! Delete.byFields SqliteDb.TableName Any [ Field.EQ "Value" "crimson" ] + let! remaining = Count.all SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] +] + +/// All tests for the SQLite library +let all = testList "Sqlite" [ + testList "Unit" [ queryTests; parametersTests ] + testSequenced <| testList "Integration" [ + configurationTests + customTests + definitionTests + documentTests + countTests + existsTests + findTests + updateTests + patchTests + removeFieldsTests + deleteTests + test "clean up database" { Configuration.useConnectionString "data source=:memory:" } + ] +] diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index de2abca..0deb585 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -1,5 +1,9 @@ module Types +type NumIdDocument = + { Key: int + Text: string } + type SubDocument = { Foo: string Bar: string } diff --git a/src/package.sh b/src/package.sh index d88421a..307f3d2 100755 --- a/src/package.sh +++ b/src/package.sh @@ -1,13 +1,16 @@ #!/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 .