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 .