From bad95888bf00ec8e2727860e0241e7e9cb8dc347 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 23 Dec 2023 18:51:00 -0500 Subject: [PATCH 01/26] WIP on Sqlite/PostgreSQL proj merge --- .../.idea/.gitignore | 13 + .../.idea.BitBadger.Documents/.idea/.name | 1 + .../.idea/indexLayout.xml | 10 + .../.idea.BitBadger.Documents/.idea/vcs.xml | 7 + src/BitBadger.Documents.sln | 40 + src/Common/BitBadger.Documents.Common.fsproj | 11 + src/Common/Library.fs | 204 +++ src/Directory.Build.props | 23 + src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 22 + src/Sqlite/Library.fs | 494 ++++++++ src/Test/Class1.cs | 12 + src/Test/Test.csproj | 13 + src/Tests/BitBadger.Documents.Tests.fsproj | 22 + src/Tests/CommonTests.fs | 143 +++ src/Tests/Program.fs | 6 + src/Tests/SqliteTests.fs | 1120 +++++++++++++++++ 16 files changed, 2141 insertions(+) create mode 100644 src/.idea/.idea.BitBadger.Documents/.idea/.gitignore create mode 100644 src/.idea/.idea.BitBadger.Documents/.idea/.name create mode 100644 src/.idea/.idea.BitBadger.Documents/.idea/indexLayout.xml create mode 100644 src/.idea/.idea.BitBadger.Documents/.idea/vcs.xml create mode 100644 src/BitBadger.Documents.sln create mode 100644 src/Common/BitBadger.Documents.Common.fsproj create mode 100644 src/Common/Library.fs create mode 100644 src/Directory.Build.props create mode 100644 src/Sqlite/BitBadger.Documents.Sqlite.fsproj create mode 100644 src/Sqlite/Library.fs create mode 100644 src/Test/Class1.cs create mode 100644 src/Test/Test.csproj create mode 100644 src/Tests/BitBadger.Documents.Tests.fsproj create mode 100644 src/Tests/CommonTests.fs create mode 100644 src/Tests/Program.fs create mode 100644 src/Tests/SqliteTests.fs diff --git a/src/.idea/.idea.BitBadger.Documents/.idea/.gitignore b/src/.idea/.idea.BitBadger.Documents/.idea/.gitignore new file mode 100644 index 0000000..0b2d7ee --- /dev/null +++ b/src/.idea/.idea.BitBadger.Documents/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/contentModel.xml +/modules.xml +/.idea.BitBadger.Documents.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/.idea/.idea.BitBadger.Documents/.idea/.name b/src/.idea/.idea.BitBadger.Documents/.idea/.name new file mode 100644 index 0000000..218932d --- /dev/null +++ b/src/.idea/.idea.BitBadger.Documents/.idea/.name @@ -0,0 +1 @@ +BitBadger.Documents \ No newline at end of file diff --git a/src/.idea/.idea.BitBadger.Documents/.idea/indexLayout.xml b/src/.idea/.idea.BitBadger.Documents/.idea/indexLayout.xml new file mode 100644 index 0000000..c88ded7 --- /dev/null +++ b/src/.idea/.idea.BitBadger.Documents/.idea/indexLayout.xml @@ -0,0 +1,10 @@ + + + + + ../../BitBadger.Documents + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.BitBadger.Documents/.idea/vcs.xml b/src/.idea/.idea.BitBadger.Documents/.idea/vcs.xml new file mode 100644 index 0000000..62bd7a0 --- /dev/null +++ b/src/.idea/.idea.BitBadger.Documents/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln new file mode 100644 index 0000000..da810bf --- /dev/null +++ b/src/BitBadger.Documents.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Common", "Common\BitBadger.Documents.Common.fsproj", "{E52D624A-2A1F-4D38-82B6-115907D9CB1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Tests", "Tests\BitBadger.Documents.Tests.fsproj", "{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite", "Sqlite\BitBadger.Documents.Sqlite.fsproj", "{B8A82483-1E72-46D2-B29A-1C371AC5DD20}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.Build.0 = Release|Any CPU + {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Release|Any CPU.Build.0 = Release|Any CPU + {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Release|Any CPU.Build.0 = Release|Any CPU + {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Common/BitBadger.Documents.Common.fsproj b/src/Common/BitBadger.Documents.Common.fsproj new file mode 100644 index 0000000..cfcbca0 --- /dev/null +++ b/src/Common/BitBadger.Documents.Common.fsproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Common/Library.fs b/src/Common/Library.fs new file mode 100644 index 0000000..8ee9204 --- /dev/null +++ b/src/Common/Library.fs @@ -0,0 +1,204 @@ +namespace BitBadger.Documents + +/// The types of logical operations available for JSON fields +[] +type Op = + /// Equals (=) + | EQ + /// Greater Than (>) + | GT + /// Greater Than or Equal To (>=) + | GE + /// Less Than (<) + | LT + /// Less Than or Equal To (<=) + | LE + /// Not Equal to (<>) + | NE + /// Exists (IS NOT NULL) + | EX + /// Does Not Exist (IS NULL) + | NEX + + override this.ToString() = + match this with + | EQ -> "=" + | GT -> ">" + | GE -> ">=" + | LT -> "<" + | LE -> "<=" + | NE -> "<>" + | EX -> "IS NOT NULL" + | NEX -> "IS NULL" + + +/// The required document serialization implementation +type IDocumentSerializer = + + /// Serialize an object to a JSON string + abstract Serialize<'T> : 'T -> string + + /// Deserialize a JSON string into an object + abstract Deserialize<'T> : string -> 'T + + +/// Document serializer defaults +module DocumentSerializer = + + open System.Text.Json + open System.Text.Json.Serialization + + /// The default JSON serializer options to use with the stock serializer + let private jsonDefaultOpts = + let o = JsonSerializerOptions() + o.Converters.Add(JsonFSharpConverter()) + o + + /// The default JSON serializer + [] + let ``default`` = + { new IDocumentSerializer with + member _.Serialize<'T>(it: 'T) : string = + JsonSerializer.Serialize(it, jsonDefaultOpts) + member _.Deserialize<'T>(it: string) : 'T = + JsonSerializer.Deserialize<'T>(it, jsonDefaultOpts) + } + + +/// Configuration for document handling +[] +module Configuration = + + /// The serializer to use for document manipulation + let mutable private serializerValue = DocumentSerializer.``default`` + + /// Register a serializer to use for translating documents to domain types + [] + let useSerializer ser = + serializerValue <- ser + + /// Retrieve the currently configured serializer + [] + let serializer () = + serializerValue + + /// The serialized name of the ID field for documents + let mutable idFieldValue = "Id" + + /// Specify the name of the ID field for documents + [] + let useIdField it = + idFieldValue <- it + + /// Retrieve the currently configured ID field for documents + [] + let idField () = + idFieldValue + + +/// 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}" + + /// Create a WHERE clause fragment to implement a comparison on a field in a JSON document + [] + let whereByField fieldName op paramName = + let theRest = + match op with + | EX | NEX -> string op + | _ -> $"{op} %s{paramName}" + $"data ->> '%s{fieldName}' {theRest}" + + /// Create a WHERE clause fragment to implement an ID-based query + [] + let whereById paramName = + whereByField (Configuration.idField ()) EQ paramName + + /// Query to insert a document + [] + let insert tableName = + $"INSERT INTO %s{tableName} VALUES (@data)" + + /// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save tableName = + sprintf + "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" + tableName (Configuration.idField ()) + + /// 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 fieldName op = + $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereByField fieldName op "@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 fieldName op = + $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereByField fieldName op "@field"}) AS it""" + + /// Queries for retrieving documents + module Find = + + /// Query to retrieve a document by its ID + [] + let byId tableName = + $"""{selectFromTable tableName} WHERE {whereById "@id"}""" + + /// Query to retrieve documents using a comparison on a JSON field + [] + let byField tableName fieldName op = + $"""{selectFromTable tableName} WHERE {whereByField fieldName op "@field"}""" + + /// Queries to update documents + module Update = + + /// Query to update a document + [] + let full tableName = + $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" + + /// Query to update a partial document by its ID + [] + let partialById tableName = + $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {whereById "@id"}""" + + /// Query to update a partial document via a comparison on a JSON field + [] + let partialByField tableName fieldName op = + sprintf + "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" + tableName (whereByField fieldName op "@field") + + /// 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 fieldName op = + $"""DELETE FROM %s{tableName} WHERE {whereByField fieldName op "@field"}""" diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..2d3771a --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,23 @@ + + + net6.0;net7.0;net8.0 + embedded + false + 1.0.0.0 + 1.0.0.0 + 1.0.0 + alpha + Initial release with F# support + danieljsummers + Bit Badger Solutions + README.md + icon.png + https://bitbadger.solutions/open-source/sqlite-documents/ + false + https://github.com/bit-badger/BitBadger.Sqlite.Documents + Git + MIT License + MIT + SQLite JSON document + + diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj new file mode 100644 index 0000000..4bdb116 --- /dev/null +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + <_Parameter1>BitBadger.Documents.Tests + + + + + + + + diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs new file mode 100644 index 0000000..d6218bd --- /dev/null +++ b/src/Sqlite/Library.fs @@ -0,0 +1,494 @@ +module BitBadger.Documents.Sqlite + +open BitBadger.Documents +open Microsoft.Data.Sqlite + +/// Configuration for document handling +module Configuration = + + /// The connection string to use for query execution + let mutable internal connectionString: string option = None + + /// Register a connection string to use for query execution (enables foreign keys) + let useConnectionString connStr = + let builder = SqliteConnectionStringBuilder(connStr) + builder.ForeignKeys <- Option.toNullable (Some true) + connectionString <- Some (string builder) + + /// Retrieve the currently configured data source + let dbConn () = + match connectionString with + | Some connStr -> + let conn = new SqliteConnection(connStr) + conn.Open() + conn + | None -> invalidOp "Please provide a connection string before attempting data access" + + +/// Execute a non-query command +let internal write (cmd: SqliteCommand) = backgroundTask { + let! _ = cmd.ExecuteNonQueryAsync() + () +} + +/// Data definition +[] +module Definition = + + /// SQL statement to create a document table + let createTable name = + $"CREATE TABLE IF NOT EXISTS %s{name} (data TEXT NOT NULL)" + + /// SQL statement to create a key index for a document table + let createKey name = + $"CREATE UNIQUE INDEX IF NOT EXISTS idx_%s{name}_key ON {name} ((data ->> '{Configuration.idField ()}'))" + + /// Definitions that take a SqliteConnection as their last parameter + module WithConn = + + /// Create a document table + let ensureTable name (conn: SqliteConnection) = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- createTable name + do! write cmd + cmd.CommandText <- createKey name + do! write cmd + } + + /// Create a document table + let ensureTable name = + use conn = Configuration.dbConn () + WithConn.ensureTable name conn + + +/// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) +let addParam (cmd: SqliteCommand) name (value: obj) = + cmd.Parameters.AddWithValue(name, value) |> ignore + +let addIdParam (cmd: SqliteCommand) (key: 'TKey) = + addParam cmd "@id" (string key) + +/// Add a JSON document parameter to a command +let addJsonParam (cmd: SqliteCommand) name (it: 'TJson) = + addParam cmd name (Configuration.serializer().Serialize it) + +/// Add ID (@id) and document (@data) parameters to a command +let addIdAndDocParams cmd (docId: 'TKey) (doc: 'TDoc) = + addIdParam cmd docId + addJsonParam cmd "@data" doc + +/// Create a domain item from a document, specifying the field in which the document is found +let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = + Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) + +/// Create a domain item from a document +let fromData<'TDoc> rdr = + fromDocument<'TDoc> "data" rdr + +/// Create a list of items for the results of the given command, using the specified mapping function +let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask { + use! rdr = cmd.ExecuteReaderAsync() + let mutable it = Seq.empty<'TDoc> + while! rdr.ReadAsync() do + it <- Seq.append it (Seq.singleton (mapFunc rdr)) + return List.ofSeq it +} + +/// Create a list of items for the results of the given command +let toDocumentList<'TDoc> (cmd: SqliteCommand) = + toCustomList<'TDoc> cmd fromData + +/// Execute a non-query statement to manipulate a document +let private executeNonQuery query (document: 'T) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + addJsonParam cmd "@data" document + write cmd + +/// Execute a non-query statement to manipulate a document with an ID specified +let private executeNonQueryWithId query (docId: 'TKey) (document: 'TDoc) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + addIdAndDocParams cmd docId document + write cmd + + +open System.Threading.Tasks + +/// Versions of queries that accept a SqliteConnection as the last parameter +module WithConn = + + /// Insert a new document + let insert<'TDoc> tableName (document: 'TDoc) conn = + executeNonQuery (Query.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) conn = + executeNonQuery (Query.save tableName) document conn + + /// Commands to count documents + [] + module Count = + + /// Count all documents in a table + let all tableName (conn: SqliteConnection) : Task = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Count.all tableName + let! result = cmd.ExecuteScalarAsync() + return result :?> int64 + } + + /// Count matching documents using a comparison on a JSON field + let byField tableName fieldName op (value: obj) (conn: SqliteConnection) : Task = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Count.byField tableName fieldName op + addParam cmd "@field" value + let! result = cmd.ExecuteScalarAsync() + return result :?> int64 + } + + /// Commands to determine if documents exist + [] + module Exists = + + /// Determine if a document exists for the given ID + let byId tableName (docId: 'TKey) (conn: SqliteConnection) : Task = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Exists.byId tableName + addIdParam cmd docId + let! result = cmd.ExecuteScalarAsync() + return (result :?> int64) > 0 + } + + /// Determine if a document exists using a comparison on a JSON field + let byField tableName fieldName op (value: obj) (conn: SqliteConnection) : Task = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Exists.byField tableName fieldName op + addParam cmd "@field" value + let! result = cmd.ExecuteScalarAsync() + return (result :?> int64) > 0 + } + + /// Commands to retrieve documents + [] + module Find = + + /// Retrieve all documents in the given table + let all<'TDoc> tableName (conn: SqliteConnection) : Task<'TDoc list> = + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.selectFromTable tableName + toDocumentList<'TDoc> cmd + + /// Retrieve a document by its ID + let byId<'TKey, 'TDoc> tableName (docId: 'TKey) (conn: SqliteConnection) : Task<'TDoc option> = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Find.byId tableName + addIdParam cmd docId + let! results = toDocumentList<'TDoc> cmd + return List.tryHead results + } + + /// Retrieve documents via a comparison on a JSON field + let byField<'TDoc> tableName fieldName op (value: obj) (conn: SqliteConnection) : Task<'TDoc list> = + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Find.byField tableName fieldName op + addParam cmd "@field" value + toDocumentList<'TDoc> cmd + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + let firstByField<'TDoc> tableName fieldName op (value: obj) (conn: SqliteConnection) + : Task<'TDoc option> = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- $"{Query.Find.byField tableName fieldName op} LIMIT 1" + addParam cmd "@field" value + let! results = toDocumentList<'TDoc> cmd + return List.tryHead results + } + + /// Commands to update documents + [] + module Update = + + /// Update an entire document + let full tableName (docId: 'TKey) (document: 'TDoc) conn = + executeNonQueryWithId (Query.Update.full tableName) docId document conn + + /// Update an entire document + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn = + full tableName (idFunc document) document conn + + /// Update a partial document + let partialById tableName (docId: 'TKey) (partial: 'TPatch) conn = + executeNonQueryWithId (Query.Update.partialById tableName) docId partial conn + + /// Update partial documents using a comparison on a JSON field + let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Update.partialByField tableName fieldName op + addParam cmd "@field" value + addJsonParam cmd "@data" partial + write cmd + + /// Commands to delete documents + [] + module Delete = + + /// Delete a document by its ID + let byId tableName (docId: 'TKey) conn = + executeNonQueryWithId (Query.Delete.byId tableName) docId {||} conn + + /// Delete documents by matching a comparison on a JSON field + let byField tableName fieldName op (value: obj) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Delete.byField tableName fieldName op + addParam cmd "@field" value + write cmd + + /// Commands to execute custom SQL queries + [] + module Custom = + + /// Execute a query that returns a list of results + let list<'TDoc> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'TDoc) + (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + cmd.Parameters.AddRange parameters + toCustomList<'TDoc> cmd mapFunc + + /// Execute a query that returns one or no results + let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) conn = backgroundTask { + let! results = list query parameters mapFunc conn + return List.tryHead results + } + + /// Execute a query that does not return a value + let nonQuery query (parameters: SqliteParameter seq) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + cmd.Parameters.AddRange parameters + write cmd + + /// Execute a query that returns a scalar value + let scalar<'T when 'T : struct> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'T) + (conn: SqliteConnection) = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- query + cmd.Parameters.AddRange parameters + use! rdr = cmd.ExecuteReaderAsync() + let! isFound = rdr.ReadAsync() + return if isFound then mapFunc rdr else Unchecked.defaultof<'T> + } + +/// Insert a new document +let insert<'TDoc> tableName (document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.insert tableName document conn + +/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") +let save<'TDoc> tableName (document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.save tableName document conn + +/// Commands to count documents +[] +module Count = + + /// Count all documents in a table + let all tableName = + use conn = Configuration.dbConn () + WithConn.Count.all tableName conn + + /// Count matching documents using a comparison on a JSON field + let byField tableName fieldName op (value: obj) = + use conn = Configuration.dbConn () + WithConn.Count.byField tableName fieldName op value conn + +/// Commands to determine if documents exist +[] +module Exists = + + /// Determine if a document exists for the given ID + 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 fieldName op (value: obj) = + use conn = Configuration.dbConn () + WithConn.Exists.byField tableName fieldName op value conn + +/// Commands to determine if documents exist +[] +module Find = + + /// Retrieve all documents in the given table + let all<'TDoc> tableName = + use conn = Configuration.dbConn () + WithConn.Find.all<'TDoc> tableName conn + + /// Retrieve a document by its ID + let byId<'TKey, 'TDoc> tableName docId = + 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 fieldName op value = + use conn = Configuration.dbConn () + WithConn.Find.byField<'TDoc> tableName fieldName op value conn + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + let firstByField<'TDoc> tableName fieldName op value = + use conn = Configuration.dbConn () + WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn + +/// Commands to update documents +[] +module Update = + + /// Update an entire document + let full tableName (docId: 'TKey) (document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Update.full tableName docId document conn + + /// Update an entire document + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Update.fullFunc tableName idFunc document conn + + /// Update a partial document + let partialById tableName (docId: 'TKey) (partial: 'TPatch) = + use conn = Configuration.dbConn () + WithConn.Update.partialById tableName docId partial conn + + /// Update partial documents using a comparison on a JSON field in the WHERE clause + let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) = + use conn = Configuration.dbConn () + WithConn.Update.partialByField tableName fieldName op value partial conn + +/// Commands to delete documents +[] +module Delete = + + /// Delete a document by its ID + 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 fieldName op (value: obj) = + use conn = Configuration.dbConn () + WithConn.Delete.byField tableName fieldName op value conn + +/// Commands to execute custom SQL queries +[] +module Custom = + + /// Execute a query that returns a list of results + let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Custom.list<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns one or no results + let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Custom.single<'TDoc> query parameters mapFunc conn + + /// Execute a query that does not return a value + let nonQuery query parameters = + use conn = Configuration.dbConn () + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + let scalar<'T when 'T : struct> query parameters (mapFunc: SqliteDataReader -> 'T) = + use conn = Configuration.dbConn () + WithConn.Custom.scalar<'T> query parameters mapFunc conn + +[] +module Extensions = + + type SqliteConnection with + + /// Create a document table + member conn.ensureTable name = + Definition.WithConn.ensureTable name conn + + /// Insert a new document + member conn.insert<'TDoc> tableName (document: 'TDoc) = + WithConn.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 + + /// 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 fieldName op (value: obj) = + WithConn.Count.byField tableName fieldName op value 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 fieldName op (value: obj) = + WithConn.Exists.byField tableName fieldName op value conn + + /// Retrieve all documents in the given table + member conn.findAll<'TDoc> tableName = + WithConn.Find.all<'TDoc> tableName 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 fieldName op (value: obj) = + WithConn.Find.byField<'TDoc> tableName fieldName op value conn + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) = + WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn + + /// Update an entire document + member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = + WithConn.Update.full tableName docId document conn + + /// Update an entire document + member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithConn.Update.fullFunc tableName idFunc document conn + + /// Update a partial document + member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPatch) = + WithConn.Update.partialById tableName docId partial conn + + /// Update partial documents using a comparison on a JSON field + member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPatch) = + WithConn.Update.partialByField tableName fieldName op value partial 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 fieldName op (value: obj) = + WithConn.Delete.byField tableName fieldName op value conn + + /// Execute a query that returns a list of results + member conn.customList<'TDoc> query parameters mapFunc = + WithConn.Custom.list<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns one or no results + member conn.customSingle<'TDoc> query parameters mapFunc = + WithConn.Custom.single<'TDoc> query parameters mapFunc conn + + /// Execute a query that does not return a value + member conn.customNonQuery query parameters = + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + member conn.customScalar<'T when 'T: struct> query parameters mapFunc = + WithConn.Custom.scalar<'T> query parameters mapFunc conn diff --git a/src/Test/Class1.cs b/src/Test/Class1.cs new file mode 100644 index 0000000..b96c733 --- /dev/null +++ b/src/Test/Class1.cs @@ -0,0 +1,12 @@ +namespace Test; + +using BitBadger.Documents; + +public class Class1 +{ + public void Toot() + { + var ticket = Query.WhereByField("test", Op.GE, ""); + var others = Query.Count.All("howdy"); + } +} diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj new file mode 100644 index 0000000..809da3f --- /dev/null +++ b/src/Test/Test.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj new file mode 100644 index 0000000..d6f064a --- /dev/null +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -0,0 +1,22 @@ + + + + Exe + + + + + + + + + + + + + + + + + + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs new file mode 100644 index 0000000..cfd3a09 --- /dev/null +++ b/src/Tests/CommonTests.fs @@ -0,0 +1,143 @@ +module CommonTests + +open BitBadger.Documents +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 "EX succeeds" { + Expect.equal (string EX) "IS NOT NULL" """The "exists" operator ws not correct""" + } + test "NEX succeeds" { + Expect.equal (string NEX) "IS NULL" """The "not exists" operator ws not correct""" + } + ] + testList "Query" [ + test "selectFromTable succeeds" { + Expect.equal (Query.selectFromTable tbl) $"SELECT data FROM {tbl}" "SELECT statement not correct" + } + test "whereById succeeds" { + Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct" + } + testList "whereByField" [ + test "succeeds when a logical operator is passed" { + Expect.equal + (Query.whereByField "theField" GT "@test") + "data ->> 'theField' > @test" + "WHERE clause not correct" + } + test "succeeds when an existence operator is passed" { + Expect.equal + (Query.whereByField "thatField" NEX "") + "data ->> 'thatField' IS NULL" + "WHERE clause not correct" + } + ] + 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" + } + 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 "thatField" EQ) + $"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 "Test" LT) + $"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 "Golf" GE) + $"SELECT data FROM {tbl} WHERE data ->> 'Golf' >= @field" + "SELECT by JSON comparison query not correct" + } + ] + testList "Update" [ + test "full succeeds" { + Expect.equal + (Query.Update.full tbl) + $"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id" + "UPDATE full statement not correct" + } + test "partialById succeeds" { + Expect.equal + (Query.Update.partialById tbl) + $"UPDATE {tbl} SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" + "UPDATE partial by ID statement not correct" + } + test "partialByField succeeds" { + Expect.equal + (Query.Update.partialByField tbl "Part" NE) + $"UPDATE {tbl} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" + "UPDATE partial by JSON comparison query not correct" + } + ] + testList "Delete" [ + test "byId succeeds" { + Expect.equal + (Query.Delete.byId tbl) + $"DELETE FROM {tbl} WHERE data ->> 'Id' = @id" + "DELETE by ID query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.Delete.byField tbl "gone" NEX) + $"DELETE FROM {tbl} WHERE data ->> 'gone' IS NULL" + "DELETE by JSON comparison query not correct" + } + ] + ] + ] + diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs new file mode 100644 index 0000000..4bd23f4 --- /dev/null +++ b/src/Tests/Program.fs @@ -0,0 +1,6 @@ +open Expecto + +let allTests = testList "BitBadger.Documents" [ CommonTests.all; SqliteTests.all ] + +[] +let main args = runTestsWithCLIArgs [] args allTests diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs new file mode 100644 index 0000000..5bf7ad3 --- /dev/null +++ b/src/Tests/SqliteTests.fs @@ -0,0 +1,1120 @@ +module SqliteTests + +type SubDocument = + { Foo: string + Bar: string } + +type JsonDocument = + { Id: string + Value: string + NumValue: int + Sub: SubDocument option } + +let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } + + +open BitBadger.Documents.Sqlite + +/// Database access for these tests +module Db = + + open System + open System.IO + open System.Threading.Tasks + + /// The table name for the catalog metadata + let catalog = "sqlite_master" + + /// The name of the table used for testing + let tableName = "test_table" + + /// A throwaway SQLite database file, which will be deleted when it goes out of scope + type ThrowawaySqliteDb(dbName: string) = + let deleteMe () = + if File.Exists dbName then File.Delete dbName + interface IDisposable with + member _.Dispose() = + deleteMe () + interface IAsyncDisposable with + member _.DisposeAsync() = + deleteMe () + ValueTask.CompletedTask + + /// Create a throwaway database file with the test_table defined + let buildDb () = task { + let dbName = $"""test-db-{Guid.NewGuid().ToString("n")}.db""" + Configuration.useConnectionString $"data source={dbName}" + do! Definition.ensureTable tableName + return new ThrowawaySqliteDb(dbName) + } + + +open BitBadger.Documents +open Expecto +open Microsoft.Data.Sqlite + +/// Tests which do not hit the database +let unitTests = + testList "Unit" [ + testList "Definition" [ + test "createTable succeeds" { + Expect.equal (Definition.createTable Db.tableName) + $"CREATE TABLE IF NOT EXISTS {Db.tableName} (data TEXT NOT NULL)" + "CREATE TABLE statement not constructed correctly" + } + test "createKey succeeds" { + Expect.equal (Definition.createKey Db.tableName) + $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{Db.tableName}_key ON {Db.tableName} ((data ->> 'Id'))" + "CREATE INDEX for key statement not constructed correctly" + } + ] + 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 "EX succeeds" { + Expect.equal (string EX) "IS NOT NULL" """The "exists" operator ws not correct""" + } + test "NEX succeeds" { + Expect.equal (string NEX) "IS NULL" """The "not exists" operator ws not correct""" + } + ] + testList "Query" [ + test "selectFromTable succeeds" { + Expect.equal + (Query.selectFromTable Db.tableName) + $"SELECT data FROM {Db.tableName}" + "SELECT statement not correct" + } + test "whereById succeeds" { + Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct" + } + testList "whereByField" [ + test "succeeds when a logical operator is passed" { + Expect.equal + (Query.whereByField "theField" GT "@test") + "data ->> 'theField' > @test" + "WHERE clause not correct" + } + test "succeeds when an existence operator is passed" { + Expect.equal + (Query.whereByField "thatField" NEX "") + "data ->> 'thatField' IS NULL" + "WHERE clause not correct" + } + ] + test "insert succeeds" { + Expect.equal + (Query.insert Db.tableName) + $"INSERT INTO {Db.tableName} VALUES (@data)" + "INSERT statement not correct" + } + test "save succeeds" { + Expect.equal + (Query.save Db.tableName) + $"INSERT INTO {Db.tableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" + "INSERT ON CONFLICT UPDATE statement not correct" + } + testList "Count" [ + test "all succeeds" { + Expect.equal + (Query.Count.all Db.tableName) + $"SELECT COUNT(*) AS it FROM {Db.tableName}" + "Count query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.Count.byField Db.tableName "thatField" EQ) + $"SELECT COUNT(*) AS it FROM {Db.tableName} WHERE data ->> 'thatField' = @field" + "JSON field text comparison count query not correct" + } + ] + testList "Exists" [ + test "byId succeeds" { + Expect.equal + (Query.Exists.byId Db.tableName) + $"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Id' = @id) AS it" + "ID existence query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.Exists.byField Db.tableName "Test" LT) + $"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Test' < @field) AS it" + "JSON field text comparison exists query not correct" + } + ] + testList "Find" [ + test "byId succeeds" { + Expect.equal + (Query.Find.byId Db.tableName) + $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + "SELECT by ID query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.Find.byField Db.tableName "Golf" GE) + $"SELECT data FROM {Db.tableName} WHERE data ->> 'Golf' >= @field" + "SELECT by JSON text comparison query not correct" + } + ] + testList "Update" [ + test "full succeeds" { + Expect.equal + (Query.Update.full Db.tableName) + $"UPDATE {Db.tableName} SET data = @data WHERE data ->> 'Id' = @id" + "UPDATE full statement not correct" + } + test "partialById succeeds" { + Expect.equal + (Query.Update.partialById Db.tableName) + $"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" + "UPDATE partial by ID statement not correct" + } + test "partialByField succeeds" { + Expect.equal + (Query.Update.partialByField Db.tableName "Part" NE) + $"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" + "UPDATE partial by JSON containment statement not correct" + } + ] + testList "Delete" [ + test "byId succeeds" { + Expect.equal + (Query.Delete.byId Db.tableName) + $"DELETE FROM {Db.tableName} WHERE data ->> 'Id' = @id" + "DELETE by ID query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.Delete.byField Db.tableName "gone" EQ) + $"DELETE FROM {Db.tableName} WHERE data ->> 'gone' = @field" + "DELETE by JSON containment query not correct" + } + ] + ] + ] + +let isTrue<'T> (_ : 'T) = true + +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 Db.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 "Definition" [ + testTask "ensureTable succeeds" { + use! db = Db.buildDb () + let itExists (name: string) = task { + let! result = + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it" + [ SqliteParameter("@name", name) ] + _.GetInt64(0) + return result > 0 + } + + 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" + } + ] + testList "insert" [ + testTask "succeeds" { + use! db = Db.buildDb () + let! before = Find.all Db.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 Db.tableName testDoc + let! after = Find.all Db.tableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use! db = Db.buildDb () + do! insert Db.tableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + insert Db.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 = Db.buildDb () + let! before = Find.all Db.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 Db.tableName testDoc + let! after = Find.all Db.tableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! insert Db.tableName testDoc + + let! before = Find.byId Db.tableName "test" + if Option.isNone before then Expect.isTrue false "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 Db.tableName upd8Doc + let! after = Find.byId Db.tableName "test" + if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + do! loadDocs () + + let! theCount = Count.all Db.tableName + Expect.equal theCount 5L "There should have been 5 matching documents" + } + testTask "byField succeeds" { + use! db = Db.buildDb () + do! loadDocs () + + let! theCount = Count.byField Db.tableName "Value" EQ "purple" + Expect.equal theCount 2L "There should have been 2 matching documents" + } + ] + testList "Exists" [ + testList "byId" [ + testTask "succeeds when a document exists" { + use! db = Db.buildDb () + do! loadDocs () + + let! exists = Exists.byId Db.tableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use! db = Db.buildDb () + do! loadDocs () + + let! exists = Exists.byId Db.tableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "byField" [ + testTask "succeeds when documents exist" { + use! db = Db.buildDb () + do! loadDocs () + + let! exists = Exists.byField Db.tableName "NumValue" EQ 10 + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use! db = Db.buildDb () + do! loadDocs () + + let! exists = Exists.byField Db.tableName "Nothing" LT "none" + Expect.isFalse exists "There should not have been any existing documents" + } + ] + ] + testList "Find" [ + testList "all" [ + testTask "succeeds when there is data" { + use! db = Db.buildDb () + + do! insert Db.tableName { Foo = "one"; Bar = "two" } + do! insert Db.tableName { Foo = "three"; Bar = "four" } + do! insert Db.tableName { Foo = "five"; Bar = "six" } + + let! results = Find.all Db.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 = Db.buildDb () + let! results = Find.all Db.tableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "byId" [ + testTask "succeeds when a document is found" { + use! db = Db.buildDb () + do! loadDocs () + + let! doc = Find.byId Db.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 = Db.buildDb () + do! loadDocs () + + let! doc = Find.byId Db.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 = Db.buildDb () + do! loadDocs () + + let! docs = Find.byField Db.tableName "NumValue" GT 15 + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use! db = Db.buildDb () + do! loadDocs () + + let! docs = Find.byField Db.tableName "NumValue" GT 100 + Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + } + ] + testList "firstByField" [ + testTask "succeeds when a document is found" { + use! db = Db.buildDb () + do! loadDocs () + + let! doc = Find.firstByField Db.tableName "Value" EQ "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 = Db.buildDb () + do! loadDocs () + + let! doc = Find.firstByField Db.tableName "Sub.Foo" EQ "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 = Db.buildDb () + do! loadDocs () + + let! doc = Find.firstByField Db.tableName "Value" EQ "absent" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + ] + testList "Update" [ + testList "full" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! Update.full Db.tableName "one" testDoc + let! after = Find.byId Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + + let! before = Find.all Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.full + Db.tableName + "test" + { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "fullFunc" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + do! loadDocs () + + do! Update.fullFunc Db.tableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + + let! before = Find.all Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.fullFunc Db.tableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] + testList "partialById" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + do! loadDocs () + + do! Update.partialById Db.tableName "one" {| NumValue = 44 |} + let! after = Find.byId Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + + let! before = Find.all Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.partialById Db.tableName "test" {| Foo = "green" |} + } + ] + testList "partialByField" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + do! loadDocs () + + do! Update.partialByField Db.tableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = Count.byField Db.tableName "NumValue" EQ 77 + Expect.equal after 2L "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use! db = Db.buildDb () + + let! before = Find.all Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.partialByField Db.tableName "Value" EQ "burgundy" {| Foo = "green" |} + } + ] + ] + testList "Delete" [ + testList "byId" [ + testTask "succeeds when a document is deleted" { + use! db = Db.buildDb () + do! loadDocs () + + do! Delete.byId Db.tableName "four" + let! remaining = Count.all Db.tableName + Expect.equal remaining 4L "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use! db = Db.buildDb () + do! loadDocs () + + do! Delete.byId Db.tableName "thirty" + let! remaining = Count.all Db.tableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "byField" [ + testTask "succeeds when documents are deleted" { + use! db = Db.buildDb () + do! loadDocs () + + do! Delete.byField Db.tableName "Value" NE "purple" + let! remaining = Count.all Db.tableName + Expect.equal remaining 2L "There should have been 2 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use! db = Db.buildDb () + do! loadDocs () + + do! Delete.byField Db.tableName "Value" EQ "crimson" + let! remaining = Count.all Db.tableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + ] + testList "Custom" [ + testList "single" [ + testTask "succeeds when a row is found" { + use! db = Db.buildDb () + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {Db.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 = Db.buildDb () + do! loadDocs () + + let! doc = + Custom.single + $"SELECT data FROM {Db.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 = Db.buildDb () + do! loadDocs () + + let! docs = Custom.list (Query.selectFromTable Db.tableName) [] fromData + Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use! db = Db.buildDb () + do! loadDocs () + + let! docs = + Custom.list + $"SELECT data FROM {Db.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 = Db.buildDb () + do! loadDocs () + + do! Custom.nonQuery $"DELETE FROM {Db.tableName}" [] + + let! remaining = Count.all Db.tableName + Expect.equal remaining 0L "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use! db = Db.buildDb () + do! loadDocs () + + do! Custom.nonQuery + $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + + let! remaining = Count.all Db.tableName + Expect.equal remaining 5L "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use! db = Db.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 "Extensions" [ + testTask "ensureTable succeeds" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + let itExists (name: string) = task { + let! result = + conn.customScalar + $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it" + [ SqliteParameter("@name", name) ] + _.GetInt64(0) + return result > 0 + } + + 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! conn.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" + } + testList "insert" [ + testTask "succeeds" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + let! before = conn.findAll Db.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! conn.insert Db.tableName testDoc + let! after = conn.findAll Db.tableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! conn.insert Db.tableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + conn.insert Db.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 = Db.buildDb () + use conn = Configuration.dbConn () + let! before = conn.findAll Db.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! conn.save Db.tableName testDoc + let! after = conn.findAll Db.tableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! conn.insert Db.tableName testDoc + + let! before = conn.findById Db.tableName "test" + if Option.isNone before then Expect.isTrue false "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! conn.save Db.tableName upd8Doc + let! after = conn.findById Db.tableName "test" + if Option.isNone after then + Expect.isTrue false "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] + testTask "countAll succeeds" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! theCount = conn.countAll Db.tableName + Expect.equal theCount 5L "There should have been 5 matching documents" + } + testTask "countByField succeeds" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! theCount = conn.countByField Db.tableName "Value" EQ "purple" + Expect.equal theCount 2L "There should have been 2 matching documents" + } + testList "existsById" [ + testTask "succeeds when a document exists" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsById Db.tableName "three" + Expect.isTrue exists "There should have been an existing document" + } + testTask "succeeds when a document does not exist" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsById Db.tableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "existsByField" [ + testTask "succeeds when documents exist" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsByField Db.tableName "NumValue" EQ 10 + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsByField Db.tableName "Nothing" EQ "none" + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "findAll" [ + testTask "succeeds when there is data" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + + do! insert Db.tableName { Foo = "one"; Bar = "two" } + do! insert Db.tableName { Foo = "three"; Bar = "four" } + do! insert Db.tableName { Foo = "five"; Bar = "six" } + + let! results = conn.findAll Db.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 = Db.buildDb () + use conn = Configuration.dbConn () + let! results = conn.findAll Db.tableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "findById" [ + testTask "succeeds when a document is found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findById Db.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 = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findById Db.tableName "three hundred eighty-seven" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + testList "findByField" [ + testTask "succeeds when documents are found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = conn.findByField Db.tableName "Sub.Foo" EQ "green" + Expect.equal (List.length docs) 2 "There should have been two documents returned" + } + testTask "succeeds when documents are not found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = conn.findByField Db.tableName "Value" EQ "mauve" + Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + } + ] + testList "findFirstByField" [ + testTask "succeeds when a document is found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField Db.tableName "Value" EQ "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 = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField Db.tableName "Sub.Foo" EQ "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 = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField Db.tableName "Value" EQ "absent" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + testList "updateFull" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! conn.updateFull Db.tableName "one" testDoc + let! after = conn.findById Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + use conn = Configuration.dbConn () + + let! before = conn.findAll Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFull + Db.tableName + "test" + { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "updateFullFunc" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updateFullFunc + Db.tableName + (_.Id) + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = conn.findById Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + use conn = Configuration.dbConn () + + let! before = conn.findAll Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFullFunc + Db.tableName + (_.Id) + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] + testList "updatePartialById" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updatePartialById Db.tableName "one" {| NumValue = 44 |} + let! after = conn.findById Db.tableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Db.buildDb () + use conn = Configuration.dbConn () + + let! before = conn.findAll Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialById Db.tableName "test" {| Foo = "green" |} + } + ] + testList "updatePartialByField" [ + testTask "succeeds when a document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updatePartialByField Db.tableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = conn.countByField Db.tableName "NumValue" EQ 77 + Expect.equal after 2L "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + + let! before = conn.findAll Db.tableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialByField Db.tableName "Value" EQ "burgundy" {| Foo = "green" |} + } + ] + testList "deleteById" [ + testTask "succeeds when a document is deleted" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteById Db.tableName "four" + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 4L "There should have been 4 documents remaining" + } + testTask "succeeds when a document is not deleted" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteById Db.tableName "thirty" + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "deleteByField" [ + testTask "succeeds when documents are deleted" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteByField Db.tableName "Value" NE "purple" + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 2L "There should have been 2 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteByField Db.tableName "Value" EQ "crimson" + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "customSingle" [ + testTask "succeeds when a row is found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.customSingle + $"SELECT data FROM {Db.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 = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.customSingle + $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + [ SqliteParameter("@id", "eighty") ] + fromData + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "customList" [ + testTask "succeeds when data is found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = conn.customList (Query.selectFromTable Db.tableName) [] fromData + Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.customList + $"SELECT data FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "customNonQuery" [ + testTask "succeeds when operating on data" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.customNonQuery $"DELETE FROM {Db.tableName}" [] + + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 0L "There should be no documents remaining in the table" + } + testTask "succeeds when no data matches where clause" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.customNonQuery + $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + + let! remaining = conn.countAll Db.tableName + Expect.equal remaining 5L "There should be 5 documents remaining in the table" + } + ] + testTask "customScalar succeeds" { + use! db = Db.buildDb () + use conn = Configuration.dbConn () + + let! nbr = conn.customScalar "SELECT 5 AS test_value" [] _.GetInt32(0) + Expect.equal nbr 5 "The query should have returned the number 5" + } + ] + test "clean up database" { + Configuration.useConnectionString "data source=:memory:" + } + ] + |> testSequenced + +let all = testList "Sqlite" [ unitTests; integrationTests ] -- 2.45.1 From 8b242d0fa374fe25111399d2a15a423062312e17 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 23 Dec 2023 23:23:55 -0500 Subject: [PATCH 02/26] Move definition queries to common lib --- src/Common/Library.fs | 29 +++++++ src/Sqlite/Library.fs | 75 +++++++++++------- src/Tests/CommonTests.fs | 30 +++++++ src/Tests/SqliteTests.fs | 167 ++------------------------------------- 4 files changed, 110 insertions(+), 191 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 8ee9204..0083626 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -119,6 +119,35 @@ module Query = let whereById paramName = whereByField (Configuration.idField ()) EQ paramName + /// Queries to define tables and indexes + module Definition = + + /// SQL statement to create a document table + let ensureTableFor name dataType = + $"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)" + + /// Split a schema and table name + let private splitSchemaAndTable (tableName: string) = + let parts = tableName.Split '.' + if Array.length parts = 1 then "", tableName else parts[0], parts[1] + + /// SQL statement to create an index on one or more fields in a JSON document + let ensureIndexOn tableName indexName (fields: string seq) = + let _, tbl = splitSchemaAndTable tableName + let jsonFields = + fields + |> Seq.map (fun it -> + 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}") + |> 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") + /// Query to insert a document [] let insert tableName = diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index d6218bd..a2d76f5 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -25,41 +25,23 @@ module Configuration = | None -> invalidOp "Please provide a connection string before attempting data access" +[] +module Query = + + /// Data definition + module Definition = + + /// SQL statement to create a document table + let ensureTable name = + Query.Definition.ensureTableFor name "TEXT" + + /// Execute a non-query command let internal write (cmd: SqliteCommand) = backgroundTask { let! _ = cmd.ExecuteNonQueryAsync() () } -/// Data definition -[] -module Definition = - - /// SQL statement to create a document table - let createTable name = - $"CREATE TABLE IF NOT EXISTS %s{name} (data TEXT NOT NULL)" - - /// SQL statement to create a key index for a document table - let createKey name = - $"CREATE UNIQUE INDEX IF NOT EXISTS idx_%s{name}_key ON {name} ((data ->> '{Configuration.idField ()}'))" - - /// Definitions that take a SqliteConnection as their last parameter - module WithConn = - - /// Create a document table - let ensureTable name (conn: SqliteConnection) = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- createTable name - do! write cmd - cmd.CommandText <- createKey name - do! write cmd - } - - /// Create a document table - let ensureTable name = - use conn = Configuration.dbConn () - WithConn.ensureTable name conn - /// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) let addParam (cmd: SqliteCommand) name (value: obj) = @@ -118,6 +100,26 @@ open System.Threading.Tasks /// Versions of queries that accept a SqliteConnection as the last parameter module WithConn = + /// Functions to create tables and indexes + [] + module Definition = + + /// Create a document table + let ensureTable name (conn: SqliteConnection) = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Definition.ensureTable name + do! write cmd + cmd.CommandText <- Query.Definition.ensureKey name + do! write cmd + } + + /// Create an index on a document table + let ensureIndex tableName indexName fields (conn: SqliteConnection) = backgroundTask { + use cmd = conn.CreateCommand() + cmd.CommandText <- Query.Definition.ensureIndexOn tableName indexName fields + do! write cmd + } + /// Insert a new document let insert<'TDoc> tableName (document: 'TDoc) conn = executeNonQuery (Query.insert tableName) document conn @@ -280,6 +282,15 @@ module WithConn = return if isFound then mapFunc rdr else Unchecked.defaultof<'T> } +/// Functions to create tables and indexes +[] +module Definition = + + /// Create a document table + let ensureTable name = + use conn = Configuration.dbConn () + WithConn.Definition.ensureTable name conn + /// Insert a new document let insert<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () @@ -411,7 +422,11 @@ module Extensions = /// Create a document table member conn.ensureTable name = - Definition.WithConn.ensureTable name conn + WithConn.Definition.ensureTable name conn + + /// Create an index on a document table + member conn.ensureIndex tableName indexName fields = + WithConn.Definition.ensureIndex tableName indexName fields conn /// Insert a new document member conn.insert<'TDoc> tableName (document: 'TDoc) = diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index cfd3a09..fff4000 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -56,6 +56,36 @@ let all = "WHERE clause 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" + } + 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" } diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 5bf7ad3..75644ed 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -49,169 +49,16 @@ module Db = } +/// A function that always returns true +let isTrue<'T> (_ : 'T) = true + + open BitBadger.Documents open Expecto open Microsoft.Data.Sqlite -/// Tests which do not hit the database -let unitTests = - testList "Unit" [ - testList "Definition" [ - test "createTable succeeds" { - Expect.equal (Definition.createTable Db.tableName) - $"CREATE TABLE IF NOT EXISTS {Db.tableName} (data TEXT NOT NULL)" - "CREATE TABLE statement not constructed correctly" - } - test "createKey succeeds" { - Expect.equal (Definition.createKey Db.tableName) - $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{Db.tableName}_key ON {Db.tableName} ((data ->> 'Id'))" - "CREATE INDEX for key statement not constructed correctly" - } - ] - 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 "EX succeeds" { - Expect.equal (string EX) "IS NOT NULL" """The "exists" operator ws not correct""" - } - test "NEX succeeds" { - Expect.equal (string NEX) "IS NULL" """The "not exists" operator ws not correct""" - } - ] - testList "Query" [ - test "selectFromTable succeeds" { - Expect.equal - (Query.selectFromTable Db.tableName) - $"SELECT data FROM {Db.tableName}" - "SELECT statement not correct" - } - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct" - } - testList "whereByField" [ - test "succeeds when a logical operator is passed" { - Expect.equal - (Query.whereByField "theField" GT "@test") - "data ->> 'theField' > @test" - "WHERE clause not correct" - } - test "succeeds when an existence operator is passed" { - Expect.equal - (Query.whereByField "thatField" NEX "") - "data ->> 'thatField' IS NULL" - "WHERE clause not correct" - } - ] - test "insert succeeds" { - Expect.equal - (Query.insert Db.tableName) - $"INSERT INTO {Db.tableName} VALUES (@data)" - "INSERT statement not correct" - } - test "save succeeds" { - Expect.equal - (Query.save Db.tableName) - $"INSERT INTO {Db.tableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" - "INSERT ON CONFLICT UPDATE statement not correct" - } - testList "Count" [ - test "all succeeds" { - Expect.equal - (Query.Count.all Db.tableName) - $"SELECT COUNT(*) AS it FROM {Db.tableName}" - "Count query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Count.byField Db.tableName "thatField" EQ) - $"SELECT COUNT(*) AS it FROM {Db.tableName} WHERE data ->> 'thatField' = @field" - "JSON field text comparison count query not correct" - } - ] - testList "Exists" [ - test "byId succeeds" { - Expect.equal - (Query.Exists.byId Db.tableName) - $"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Id' = @id) AS it" - "ID existence query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Exists.byField Db.tableName "Test" LT) - $"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Test' < @field) AS it" - "JSON field text comparison exists query not correct" - } - ] - testList "Find" [ - test "byId succeeds" { - Expect.equal - (Query.Find.byId Db.tableName) - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" - "SELECT by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Find.byField Db.tableName "Golf" GE) - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Golf' >= @field" - "SELECT by JSON text comparison query not correct" - } - ] - testList "Update" [ - test "full succeeds" { - Expect.equal - (Query.Update.full Db.tableName) - $"UPDATE {Db.tableName} SET data = @data WHERE data ->> 'Id' = @id" - "UPDATE full statement not correct" - } - test "partialById succeeds" { - Expect.equal - (Query.Update.partialById Db.tableName) - $"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "partialByField succeeds" { - Expect.equal - (Query.Update.partialByField Db.tableName "Part" NE) - $"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" - "UPDATE partial by JSON containment statement not correct" - } - ] - testList "Delete" [ - test "byId succeeds" { - Expect.equal - (Query.Delete.byId Db.tableName) - $"DELETE FROM {Db.tableName} WHERE data ->> 'Id' = @id" - "DELETE by ID query not correct" - } - test "byField succeeds" { - Expect.equal - (Query.Delete.byField Db.tableName "gone" EQ) - $"DELETE FROM {Db.tableName} WHERE data ->> 'gone' = @field" - "DELETE by JSON containment query not correct" - } - ] - ] - ] - -let isTrue<'T> (_ : 'T) = true - -let integrationTests = +/// These tests each use a fresh copy of a SQLite database +let all = let documents = [ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } @@ -1116,5 +963,3 @@ let integrationTests = } ] |> testSequenced - -let all = testList "Sqlite" [ unitTests; integrationTests ] -- 2.45.1 From f5ef2ef1d52cfa3e5e6cb6891197d6e3c2fd3fa9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 24 Dec 2023 09:46:21 -0500 Subject: [PATCH 03/26] WIP on using Custom for all other queries --- src/Sqlite/Library.fs | 417 +++++++++++++++++++++++------------------- 1 file changed, 233 insertions(+), 184 deletions(-) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index a2d76f5..dd1063d 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -10,12 +10,14 @@ module Configuration = let mutable internal connectionString: string option = None /// Register a connection string to use for query execution (enables foreign keys) + [] let useConnectionString connStr = let builder = SqliteConnectionStringBuilder(connStr) builder.ForeignKeys <- Option.toNullable (Some true) connectionString <- Some (string builder) /// Retrieve the currently configured data source + [] let dbConn () = match connectionString with | Some connStr -> @@ -32,42 +34,38 @@ module Query = module Definition = /// SQL statement to create a document table + [] let ensureTable name = Query.Definition.ensureTableFor name "TEXT" -/// Execute a non-query command -let internal write (cmd: SqliteCommand) = backgroundTask { - let! _ = cmd.ExecuteNonQueryAsync() - () -} +/// Create an ID parameter (key will be treated as a string) +[] +let idParam (key: 'TKey) = + SqliteParameter("@id", string key) +/// Create a parameter with a JSON value +[] +let jsonParam name (it: 'TJson) = + SqliteParameter(name, Configuration.serializer().Serialize it) -/// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) -let addParam (cmd: SqliteCommand) name (value: obj) = - cmd.Parameters.AddWithValue(name, value) |> ignore - -let addIdParam (cmd: SqliteCommand) (key: 'TKey) = - addParam cmd "@id" (string key) - -/// Add a JSON document parameter to a command -let addJsonParam (cmd: SqliteCommand) name (it: 'TJson) = - addParam cmd name (Configuration.serializer().Serialize it) - -/// Add ID (@id) and document (@data) parameters to a command -let addIdAndDocParams cmd (docId: 'TKey) (doc: 'TDoc) = - addIdParam cmd docId - addJsonParam cmd "@data" doc +/// Create a JSON field parameter +[] +let fieldParam (value: obj) = + SqliteParameter("@field", value) /// Create a domain item from a document, specifying the field in which the document is found +[] let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) - + /// Create a domain item from a document +[] let fromData<'TDoc> rdr = fromDocument<'TDoc> "data" rdr /// Create a list of items for the results of the given command, using the specified mapping function +[] let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask { use! rdr = cmd.ExecuteReaderAsync() let mutable it = Seq.empty<'TDoc> @@ -77,194 +75,95 @@ let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc } /// Create a list of items for the results of the given command +[] let toDocumentList<'TDoc> (cmd: SqliteCommand) = toCustomList<'TDoc> cmd fromData -/// Execute a non-query statement to manipulate a document -let private executeNonQuery query (document: 'T) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- query - addJsonParam cmd "@data" document - write cmd - -/// Execute a non-query statement to manipulate a document with an ID specified -let private executeNonQueryWithId query (docId: 'TKey) (document: 'TDoc) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- query - addIdAndDocParams cmd docId document - write cmd +/// Execute a non-query command +let internal write (cmd: SqliteCommand) = backgroundTask { + let! _ = cmd.ExecuteNonQueryAsync() + () +} -open System.Threading.Tasks +/// Command creation helper functions +[] +module private Helpers = + + let addParam (cmd: SqliteCommand) it = + cmd.Parameters.Add it |> ignore + + /// Add an ID parameter to a command + let addIdParam (cmd: SqliteCommand) (key: 'TKey) = + addParam cmd (idParam key) + + /// Add a JSON document parameter to a command + let addJsonParam (cmd: SqliteCommand) name (it: 'TJson) = + addParam cmd (jsonParam name it) + + /// Add ID (@id) and document (@data) parameters to a command + let addIdAndDocParams cmd (docId: 'TKey) (doc: 'TDoc) = + addIdParam cmd docId + addJsonParam cmd "@data" doc + + /// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) + let addFieldParam (cmd: SqliteCommand) (value: obj) = + addParam cmd (SqliteParameter("@field", value)) + + /// Execute a non-query statement to manipulate a document + let executeNonQuery query (document: 'T) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + addJsonParam cmd "@data" document + write cmd + + /// Execute a non-query statement to manipulate a document with an ID specified + let executeNonQueryWithId query (docId: 'TKey) (document: 'TDoc) (conn: SqliteConnection) = + use cmd = conn.CreateCommand() + cmd.CommandText <- query + addIdAndDocParams cmd docId document + write cmd + /// Versions of queries that accept a SqliteConnection as the last parameter module WithConn = - /// Functions to create tables and indexes - [] - module Definition = - - /// Create a document table - let ensureTable name (conn: SqliteConnection) = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Definition.ensureTable name - do! write cmd - cmd.CommandText <- Query.Definition.ensureKey name - do! write cmd - } - - /// Create an index on a document table - let ensureIndex tableName indexName fields (conn: SqliteConnection) = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Definition.ensureIndexOn tableName indexName fields - do! write cmd - } - - /// Insert a new document - let insert<'TDoc> tableName (document: 'TDoc) conn = - executeNonQuery (Query.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) conn = - executeNonQuery (Query.save tableName) document conn - - /// Commands to count documents - [] - module Count = - - /// Count all documents in a table - let all tableName (conn: SqliteConnection) : Task = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Count.all tableName - let! result = cmd.ExecuteScalarAsync() - return result :?> int64 - } - - /// Count matching documents using a comparison on a JSON field - let byField tableName fieldName op (value: obj) (conn: SqliteConnection) : Task = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Count.byField tableName fieldName op - addParam cmd "@field" value - let! result = cmd.ExecuteScalarAsync() - return result :?> int64 - } - - /// Commands to determine if documents exist - [] - module Exists = - - /// Determine if a document exists for the given ID - let byId tableName (docId: 'TKey) (conn: SqliteConnection) : Task = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Exists.byId tableName - addIdParam cmd docId - let! result = cmd.ExecuteScalarAsync() - return (result :?> int64) > 0 - } - - /// Determine if a document exists using a comparison on a JSON field - let byField tableName fieldName op (value: obj) (conn: SqliteConnection) : Task = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Exists.byField tableName fieldName op - addParam cmd "@field" value - let! result = cmd.ExecuteScalarAsync() - return (result :?> int64) > 0 - } - - /// Commands to retrieve documents - [] - module Find = - - /// Retrieve all documents in the given table - let all<'TDoc> tableName (conn: SqliteConnection) : Task<'TDoc list> = - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.selectFromTable tableName - toDocumentList<'TDoc> cmd - - /// Retrieve a document by its ID - let byId<'TKey, 'TDoc> tableName (docId: 'TKey) (conn: SqliteConnection) : Task<'TDoc option> = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Find.byId tableName - addIdParam cmd docId - let! results = toDocumentList<'TDoc> cmd - return List.tryHead results - } - - /// Retrieve documents via a comparison on a JSON field - let byField<'TDoc> tableName fieldName op (value: obj) (conn: SqliteConnection) : Task<'TDoc list> = - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Find.byField tableName fieldName op - addParam cmd "@field" value - toDocumentList<'TDoc> cmd - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - let firstByField<'TDoc> tableName fieldName op (value: obj) (conn: SqliteConnection) - : Task<'TDoc option> = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.Find.byField tableName fieldName op} LIMIT 1" - addParam cmd "@field" value - let! results = toDocumentList<'TDoc> cmd - return List.tryHead results - } - - /// Commands to update documents - [] - module Update = - - /// Update an entire document - let full tableName (docId: 'TKey) (document: 'TDoc) conn = - executeNonQueryWithId (Query.Update.full tableName) docId document conn - - /// Update an entire document - let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn = - full tableName (idFunc document) document conn - - /// Update a partial document - let partialById tableName (docId: 'TKey) (partial: 'TPatch) conn = - executeNonQueryWithId (Query.Update.partialById tableName) docId partial conn - - /// Update partial documents using a comparison on a JSON field - let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Update.partialByField tableName fieldName op - addParam cmd "@field" value - addJsonParam cmd "@data" partial - write cmd - - /// Commands to delete documents - [] - module Delete = - - /// Delete a document by its ID - let byId tableName (docId: 'TKey) conn = - executeNonQueryWithId (Query.Delete.byId tableName) docId {||} conn - - /// Delete documents by matching a comparison on a JSON field - let byField tableName fieldName op (value: obj) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.Delete.byField tableName fieldName op - addParam cmd "@field" value - write cmd - /// Commands to execute custom SQL queries [] module Custom = /// Execute a query that returns a list of results + [] let list<'TDoc> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'TDoc) (conn: SqliteConnection) = use cmd = conn.CreateCommand() cmd.CommandText <- query cmd.Parameters.AddRange parameters toCustomList<'TDoc> cmd mapFunc - - /// Execute a query that returns one or no results + + /// Execute a query that returns a list of results + let List<'TDoc>(query, parameters, mapFunc: System.Func, conn) = backgroundTask { + let! results = list<'TDoc> query parameters mapFunc.Invoke conn + return ResizeArray<'TDoc> results + } + + /// Execute a query that returns one or no results (returns None if not found) + [] let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) conn = backgroundTask { let! results = list query parameters mapFunc conn - return List.tryHead results + return FSharp.Collections.List.tryHead results + } + + /// Execute a query that returns one or no results (returns null if not found) + let Single<'TDoc when 'TDoc: null>( + query, parameters, mapFunc: System.Func, conn + ) = backgroundTask { + let! result = single<'TDoc> query parameters mapFunc.Invoke conn + return Option.toObj result } /// Execute a query that does not return a value + [] let nonQuery query (parameters: SqliteParameter seq) (conn: SqliteConnection) = use cmd = conn.CreateCommand() cmd.CommandText <- query @@ -272,6 +171,7 @@ module WithConn = write cmd /// Execute a query that returns a scalar value + [] let scalar<'T when 'T : struct> query (parameters: SqliteParameter seq) (mapFunc: SqliteDataReader -> 'T) (conn: SqliteConnection) = backgroundTask { use cmd = conn.CreateCommand() @@ -282,6 +182,155 @@ module WithConn = return if isFound then mapFunc rdr else Unchecked.defaultof<'T> } + /// Execute a query that returns a scalar value + let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func, conn) = + scalar<'T> query parameters mapFunc.Invoke conn + + /// Functions to create tables and indexes + [] + module Definition = + + /// Create a document table + [] + let ensureTable name conn = backgroundTask { + do! Custom.nonQuery (Query.Definition.ensureTable name) [] conn + do! Custom.nonQuery (Query.Definition.ensureKey name) [] conn + } + + /// Create an index on a document table + [] + let ensureIndex tableName indexName fields conn = + Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] 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 count documents + [] + module Count = + + /// Count all documents in a table + [] + let all tableName conn = + Custom.scalar (Query.Count.all tableName) [] (_.GetInt64(0)) conn + + /// Count matching documents using a comparison on a JSON field + [] + let byField tableName fieldName op (value: obj) conn = + Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] (_.GetInt64(0)) conn + + /// Commands to determine if documents exist + [] + module Exists = + + /// SQLite returns a 0 for not-exists and 1 for exists + let private exists (rdr: SqliteDataReader) = + rdr.GetInt64(0) > 0 + + /// Determine if a document exists for the given ID + [] + let byId tableName (docId: 'TKey) conn = + Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] exists conn + + /// Determine if a document exists using a comparison on a JSON field + [] + let byField tableName fieldName op (value: obj) conn = + Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] exists conn + + /// Commands to retrieve documents + [] + module Find = + + /// Retrieve all documents in the given table + [] + let all<'TDoc> tableName conn = + Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> conn + + /// Retrieve all documents in the given table + let All<'TDoc>(tableName, conn) = + Custom.List(Query.selectFromTable tableName, [], fromData<'TDoc>, conn) + + /// Retrieve a document by its ID (returns None if not found) + [] + let byId<'TKey, 'TDoc> tableName (docId: 'TKey) conn = + Custom.single<'TDoc> (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> conn + + /// Retrieve a document by its ID (returns null if not found) + let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = + Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, conn) + + /// Retrieve documents via a comparison on a JSON field + [] + let byField<'TDoc> tableName fieldName op (value: obj) conn = + Custom.list<'TDoc> (Query.Find.byField tableName fieldName op) [ fieldParam value ] fromData<'TDoc> conn + + /// Retrieve documents via a comparison on a JSON field + let ByField<'TDoc>(tableName, fieldName, op, value: obj, conn) = + Custom.List<'TDoc>(Query.Find.byField tableName fieldName op, [ fieldParam value ], fromData<'TDoc>, conn) + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] + let firstByField<'TDoc> tableName fieldName op (value: obj) conn = + Custom.single + $"{Query.Find.byField tableName fieldName op} LIMIT 1" [ fieldParam value ] fromData<'TDoc> conn + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj, conn) = + Custom.Single( + $"{Query.Find.byField tableName fieldName op} LIMIT 1", [ fieldParam value ], fromData<'TDoc>, conn) + + /// Commands to update documents + [] + module Update = + + /// Update an entire document + [] + let full tableName (docId: 'TKey) (document: 'TDoc) conn = + Custom.nonQuery (Query.Update.full tableName) [ idParam docId; jsonParam "@data" document ] conn + + /// Update an entire document + [] + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn = + full tableName (idFunc document) document conn + + /// Update an entire document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) = + fullFunc tableName idFunc.Invoke document conn + + /// Update a partial document + [] + let partialById tableName (docId: 'TKey) (partial: 'TPatch) conn = + Custom.nonQuery (Query.Update.partialById tableName) [ idParam docId; jsonParam "@data" partial ] conn + + /// Update partial documents using a comparison on a JSON field + [] + let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) (conn: SqliteConnection) = + Custom.nonQuery + (Query.Update.partialByField tableName fieldName op) + [ fieldParam value; jsonParam "@data" partial ] + conn + + /// Commands to delete documents + [] + module Delete = + + /// 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 fieldName op (value: obj) conn = + Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] conn + /// Functions to create tables and indexes [] module Definition = -- 2.45.1 From c386f670bc2991f48d76140c8f595a84e617a046 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 24 Dec 2023 12:27:22 -0500 Subject: [PATCH 04/26] WIP on C#/F# combined SQLite library --- src/Sqlite/Library.fs | 386 ++++++++++++++++++++++++++++++------------ 1 file changed, 276 insertions(+), 110 deletions(-) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index dd1063d..6cf6dc9 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -1,4 +1,5 @@ -module BitBadger.Documents.Sqlite +/// Document store implementation for SQLite +module BitBadger.Documents.Sqlite open BitBadger.Documents open Microsoft.Data.Sqlite @@ -27,6 +28,7 @@ module Configuration = | None -> invalidOp "Please provide a connection string before attempting data access" +/// Query definitions [] module Query = @@ -39,45 +41,65 @@ module Query = Query.Definition.ensureTableFor name "TEXT" -/// Create an ID parameter (key will be treated as a string) -[] -let idParam (key: 'TKey) = - SqliteParameter("@id", string key) +/// Parameter handling helpers +[] +module Parameters = + + /// Create an ID parameter (name "@id", key will be treated as a string) + [] + let idParam (key: 'TKey) = + SqliteParameter("@id", string key) -/// Create a parameter with a JSON value -[] -let jsonParam name (it: 'TJson) = - SqliteParameter(name, Configuration.serializer().Serialize it) + /// Create a parameter with a JSON value + [] + let jsonParam name (it: 'TJson) = + SqliteParameter(name, Configuration.serializer().Serialize it) -/// Create a JSON field parameter -[] -let fieldParam (value: obj) = - SqliteParameter("@field", value) + /// Create a JSON field parameter (name "@field") + [] + let fieldParam (value: obj) = + SqliteParameter("@field", value) -/// Create a domain item from a document, specifying the field in which the document is found -[] -let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = - Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) -/// Create a domain item from a document -[] -let fromData<'TDoc> rdr = - fromDocument<'TDoc> "data" rdr +/// Helper functions for handling results +[] +module Results = + + /// Create a domain item from a document, specifying the field in which the document is found + [] + let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = + Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) -/// Create a list of items for the results of the given command, using the specified mapping function -[] -let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask { - use! rdr = cmd.ExecuteReaderAsync() - let mutable it = Seq.empty<'TDoc> - while! rdr.ReadAsync() do - it <- Seq.append it (Seq.singleton (mapFunc rdr)) - return List.ofSeq it -} + /// Create a domain item from a document + [] + let fromData<'TDoc> rdr = + fromDocument<'TDoc> "data" rdr + + /// Create a list of items for the results of the given command, using the specified mapping function + [] + let toCustomList<'TDoc> (cmd: SqliteCommand) (mapFunc: SqliteDataReader -> 'TDoc) = backgroundTask { + use! rdr = cmd.ExecuteReaderAsync() + let mutable it = Seq.empty<'TDoc> + while! rdr.ReadAsync() do + it <- Seq.append it (Seq.singleton (mapFunc rdr)) + return List.ofSeq it + } + + /// Create a list of items for the results of the given command, using the specified mapping function + let ToCustomList<'TDoc>(cmd, mapFunc: System.Func) = backgroundTask { + let! results = toCustomList<'TDoc> cmd mapFunc.Invoke + return ResizeArray<'TDoc> results + } + + /// Create a list of items for the results of the given command + [] + let toDocumentList<'TDoc> (cmd: SqliteCommand) = + toCustomList<'TDoc> cmd fromData + + /// Create a list of items for the results of the given command + let ToDocumentList<'TDoc> cmd = + ToCustomList<'TDoc>(cmd, fromData<'TDoc>) -/// Create a list of items for the results of the given command -[] -let toDocumentList<'TDoc> (cmd: SqliteCommand) = - toCustomList<'TDoc> cmd fromData /// Execute a non-query command let internal write (cmd: SqliteCommand) = backgroundTask { @@ -86,45 +108,6 @@ let internal write (cmd: SqliteCommand) = backgroundTask { } -/// Command creation helper functions -[] -module private Helpers = - - let addParam (cmd: SqliteCommand) it = - cmd.Parameters.Add it |> ignore - - /// Add an ID parameter to a command - let addIdParam (cmd: SqliteCommand) (key: 'TKey) = - addParam cmd (idParam key) - - /// Add a JSON document parameter to a command - let addJsonParam (cmd: SqliteCommand) name (it: 'TJson) = - addParam cmd (jsonParam name it) - - /// Add ID (@id) and document (@data) parameters to a command - let addIdAndDocParams cmd (docId: 'TKey) (doc: 'TDoc) = - addIdParam cmd docId - addJsonParam cmd "@data" doc - - /// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) - let addFieldParam (cmd: SqliteCommand) (value: obj) = - addParam cmd (SqliteParameter("@field", value)) - - /// Execute a non-query statement to manipulate a document - let executeNonQuery query (document: 'T) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- query - addJsonParam cmd "@data" document - write cmd - - /// Execute a non-query statement to manipulate a document with an ID specified - let executeNonQueryWithId query (docId: 'TKey) (document: 'TDoc) (conn: SqliteConnection) = - use cmd = conn.CreateCommand() - cmd.CommandText <- query - addIdAndDocParams cmd docId document - write cmd - - /// Versions of queries that accept a SqliteConnection as the last parameter module WithConn = @@ -331,21 +314,68 @@ module WithConn = let byField tableName fieldName op (value: obj) conn = Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] conn + +/// Commands to execute custom SQL queries +[] +module Custom = + + /// Execute a query that returns a list of results + [] + let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Custom.list<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns a list of results + let List<'TDoc>(query, parameters, mapFunc: System.Func) = + use conn = Configuration.dbConn () + WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that returns one or no results (returns None if not found) + [] + let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Custom.single<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns one or no results (returns null if not found) + let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func) = + use conn = Configuration.dbConn () + WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that does not return a value + [] + let nonQuery query parameters = + use conn = Configuration.dbConn () + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + [] + let scalar<'T when 'T: struct> query parameters (mapFunc: SqliteDataReader -> 'T) = + use conn = Configuration.dbConn () + WithConn.Custom.scalar<'T> query parameters mapFunc conn + + /// Execute a query that returns a scalar value + let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func) = + use conn = Configuration.dbConn () + WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) + /// Functions to create tables and indexes [] module Definition = /// Create a document table + [] let ensureTable name = use conn = Configuration.dbConn () WithConn.Definition.ensureTable name conn /// Insert a new document +[] let insert<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () WithConn.insert tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") +[] let save<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () WithConn.save tableName document conn @@ -355,11 +385,13 @@ let save<'TDoc> tableName (document: 'TDoc) = module Count = /// Count all documents in a table + [] let all tableName = use conn = Configuration.dbConn () WithConn.Count.all tableName conn /// Count matching documents using a comparison on a JSON field + [] let byField tableName fieldName op (value: obj) = use conn = Configuration.dbConn () WithConn.Count.byField tableName fieldName op value conn @@ -369,11 +401,13 @@ module Count = module Exists = /// Determine if a document exists for the given ID + [] 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 fieldName op (value: obj) = use conn = Configuration.dbConn () WithConn.Exists.byField tableName fieldName op value conn @@ -383,45 +417,78 @@ module Exists = module Find = /// Retrieve all documents in the given table + [] let all<'TDoc> tableName = use conn = Configuration.dbConn () WithConn.Find.all<'TDoc> tableName conn - /// Retrieve a document by its ID + /// Retrieve all documents in the given table + let All<'TDoc> tableName = + use conn = Configuration.dbConn () + WithConn.Find.All<'TDoc>(tableName, conn) + + /// Retrieve a document by its ID (returns None if not found) + [] let byId<'TKey, 'TDoc> tableName docId = use conn = Configuration.dbConn () WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn + /// Retrieve a document by its ID (returns null if not found) + let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId) = + use conn = Configuration.dbConn () + WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) + /// Retrieve documents via a comparison on a JSON field + [] let byField<'TDoc> tableName fieldName op value = use conn = Configuration.dbConn () WithConn.Find.byField<'TDoc> tableName fieldName op value conn + /// Retrieve documents via a comparison on a JSON field + let ByField<'TDoc>(tableName, fieldName, op, value) = + use conn = Configuration.dbConn () + WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn) + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] let firstByField<'TDoc> tableName fieldName op value = use conn = Configuration.dbConn () WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn + /// Retrieve documents via a comparison on a JSON field, returning only the first result + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value) = + use conn = Configuration.dbConn () + WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn) + /// Commands to update documents [] module Update = /// Update an entire document + [] let full tableName (docId: 'TKey) (document: 'TDoc) = use conn = Configuration.dbConn () WithConn.Update.full tableName docId document conn /// Update an entire document + [] let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = use conn = Configuration.dbConn () WithConn.Update.fullFunc tableName idFunc document conn + /// Update an entire document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.Update.FullFunc(tableName, idFunc, document, conn) + /// Update a partial document + [] let partialById tableName (docId: 'TKey) (partial: 'TPatch) = use conn = Configuration.dbConn () WithConn.Update.partialById tableName docId partial conn /// Update partial documents using a comparison on a JSON field in the WHERE clause + [] let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) = use conn = Configuration.dbConn () WithConn.Update.partialByField tableName fieldName op value partial conn @@ -431,44 +498,40 @@ module Update = module Delete = /// Delete a document by its ID + [] 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 fieldName op (value: obj) = use conn = Configuration.dbConn () WithConn.Delete.byField tableName fieldName op value conn -/// Commands to execute custom SQL queries -[] -module Custom = - - /// Execute a query that returns a list of results - let list<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = - use conn = Configuration.dbConn () - WithConn.Custom.list<'TDoc> query parameters mapFunc conn - - /// Execute a query that returns one or no results - let single<'TDoc> query parameters (mapFunc: SqliteDataReader -> 'TDoc) = - use conn = Configuration.dbConn () - WithConn.Custom.single<'TDoc> query parameters mapFunc conn - - /// Execute a query that does not return a value - let nonQuery query parameters = - use conn = Configuration.dbConn () - WithConn.Custom.nonQuery query parameters conn - - /// Execute a query that returns a scalar value - let scalar<'T when 'T : struct> query parameters (mapFunc: SqliteDataReader -> 'T) = - use conn = Configuration.dbConn () - WithConn.Custom.scalar<'T> query parameters mapFunc conn +/// F# extensions for the SqliteConnection type [] module Extensions = type SqliteConnection with + /// Execute a query that returns a list of results + member conn.customList<'TDoc> query parameters mapFunc = + WithConn.Custom.list<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns one or no results + member conn.customSingle<'TDoc> query parameters mapFunc = + WithConn.Custom.single<'TDoc> query parameters mapFunc conn + + /// Execute a query that does not return a value + member conn.customNonQuery query parameters = + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + member conn.customScalar<'T when 'T: struct> query parameters mapFunc = + WithConn.Custom.scalar<'T> query parameters mapFunc conn + /// Create a document table member conn.ensureTable name = WithConn.Definition.ensureTable name conn @@ -541,18 +604,121 @@ module Extensions = member conn.deleteByField tableName fieldName op (value: obj) = WithConn.Delete.byField tableName fieldName op value conn - /// Execute a query that returns a list of results - member conn.customList<'TDoc> query parameters mapFunc = - WithConn.Custom.list<'TDoc> query parameters mapFunc conn - /// Execute a query that returns one or no results - member conn.customSingle<'TDoc> query parameters mapFunc = - WithConn.Custom.single<'TDoc> query parameters mapFunc conn - - /// Execute a query that does not return a value - member conn.customNonQuery query parameters = - WithConn.Custom.nonQuery query parameters conn +open System.Runtime.CompilerServices - /// Execute a query that returns a scalar value - member conn.customScalar<'T when 'T: struct> query parameters mapFunc = - WithConn.Custom.scalar<'T> query parameters mapFunc conn +/// C# extensions on the SqliteConnection type +[] +type SqliteConnectionCSharpExtensions = + + /// Execute a query that returns a list of results + [] + static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that returns one or no results + [] + static member inline CustomSingle<'TDoc when 'TDoc: null>( + conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that does not return a value + [] + static member inline CustomNonQuery(conn, query, parameters) = + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + [] + static member inline CustomScalar<'T when 'T: struct>( + conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) + + /// Create a document table + [] + static member inline EnsureTable(conn, name) = + WithConn.Definition.ensureTable name conn + + /// Create an index on one or more fields in a document table + [] + static member inline EnsureIndex(conn, tableName, indexName, fields) = + WithConn.Definition.ensureIndex tableName indexName fields conn + + /// Insert a new document + [] + static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = + WithConn.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 + + /// 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 + [] + static member inline CountByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Count.byField tableName fieldName op value 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 + [] + static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Exists.byField tableName fieldName op value conn + + /// Retrieve all documents in the given table + [] + static member inline FindAll<'TDoc>(conn, tableName) = + WithConn.Find.All<'TDoc>(tableName, 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 + [] + static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value) = + WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn) + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) = + WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn) + + /// Update an entire document + [] + static member inline UpdateFull<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = + WithConn.Update.full tableName docId document conn + + /// Update an entire document + [] + static member inline UpdateFullFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) = + WithConn.Update.FullFunc(tableName, idFunc, doc, conn) + + /// Update a partial document + [] + static member inline UpdatePartialById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, partial: 'TPatch) = + WithConn.Update.partialById tableName docId partial conn + + /// Update partial documents using a comparison on a JSON field + [] + static member inline UpdatePartialByField<'TPatch>(conn, tableName, fieldName, op, value: obj, partial: 'TPatch) = + WithConn.Update.partialByField tableName fieldName op value partial 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 a JSON field + [] + static member inline DeleteByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Delete.byField tableName fieldName op value conn -- 2.45.1 From 3f4aeb9f6523f0d6c18280cf6f6c0fe45b21df3d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 24 Dec 2023 21:37:33 -0500 Subject: [PATCH 05/26] WIP on C# unit tests --- src/BitBadger.Documents.sln | 6 + src/Common/Library.fs | 3 + src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 3 + src/Sqlite/Library.fs | 1 - src/Test/Test.csproj | 1 + .../BitBadger.Documents.Tests.CSharp.csproj | 17 + src/Tests.CSharp/CommonCSharpTests.cs | 253 +++++ src/Tests.CSharp/SqliteCSharpTests.cs | 939 ++++++++++++++++++ src/Tests.CSharp/SqliteDb.cs | 59 ++ src/Tests.CSharp/Types.cs | 15 + src/Tests/BitBadger.Documents.Tests.fsproj | 1 + src/Tests/CommonTests.fs | 4 +- src/Tests/Program.fs | 9 +- 13 files changed, 1307 insertions(+), 4 deletions(-) create mode 100644 src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj create mode 100644 src/Tests.CSharp/CommonCSharpTests.cs create mode 100644 src/Tests.CSharp/SqliteCSharpTests.cs create mode 100644 src/Tests.CSharp/SqliteDb.cs create mode 100644 src/Tests.CSharp/Types.cs diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln index da810bf..f3d49d0 100644 --- a/src/BitBadger.Documents.sln +++ b/src/BitBadger.Documents.sln @@ -11,6 +11,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Tests", EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite", "Sqlite\BitBadger.Documents.Sqlite.fsproj", "{B8A82483-1E72-46D2-B29A-1C371AC5DD20}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.CSharp", "Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj", "{AB58418C-7F90-467E-8F67-F4E0AD9D8875}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8A82483-1E72-46D2-B29A-1C371AC5DD20}.Release|Any CPU.Build.0 = Release|Any CPU + {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 0083626..7123a52 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -123,6 +123,7 @@ module Query = module Definition = /// SQL statement to create a document table + [] let ensureTableFor name dataType = $"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)" @@ -132,6 +133,7 @@ module Query = if Array.length parts = 1 then "", tableName else parts[0], parts[1] /// SQL statement to create an index on one or more fields in a JSON document + [] let ensureIndexOn tableName indexName (fields: string seq) = let _, tbl = splitSchemaAndTable tableName let jsonFields = @@ -145,6 +147,7 @@ module Query = $"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") diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index 4bdb116..9a59aed 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -13,6 +13,9 @@ <_Parameter1>BitBadger.Documents.Tests + + <_Parameter1>BitBadger.Documents.Tests.CSharp + diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 6cf6dc9..1ef745f 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -608,7 +608,6 @@ module Extensions = open System.Runtime.CompilerServices /// C# extensions on the SqliteConnection type -[] type SqliteConnectionCSharpExtensions = /// Execute a query that returns a list of results diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj index 809da3f..7153813 100644 --- a/src/Test/Test.csproj +++ b/src/Test/Test.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj new file mode 100644 index 0000000..c70e5b0 --- /dev/null +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -0,0 +1,17 @@ + + + + enable + enable + + + + + + + + + + + + diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs new file mode 100644 index 0000000..a90cb8f --- /dev/null +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -0,0 +1,253 @@ +namespace BitBadger.Documents.Tests.CSharp; + +using Documents; +using Expecto.CSharp; +using Expecto; + +/// +/// A test serializer that returns known values +/// +internal class TestSerializer : IDocumentSerializer +{ + public string Serialize(T it) => "{\"Overridden\":true}"; + public T Deserialize(string it) => default!; +} + +/// +/// C# Tests for common functionality in BitBadger.Documents +/// +public static class CommonCSharpTests +{ + /// + /// Unit tests + /// + [Tests] public static Test Unit = + Runner.TestList("Common.C# Unit", new[] + { + Runner.TestSequenced( + Runner.TestList("Configuration", new[] + { + Runner.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); + } + }), + Runner.TestCase("Serializer returns configured serializer", () => + { + Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), + "Serializer should have been the same"); + }), + Runner.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"); + } + }) + })), + Runner.TestList("Op", new[] + { + Runner.TestCase("EQ succeeds", () => + { + Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); + }), + Runner.TestCase("GT succeeds", () => + { + Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); + }), + Runner.TestCase("GE succeeds", () => + { + Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); + }), + Runner.TestCase("LT succeeds", () => + { + Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); + }), + Runner.TestCase("LE succeeds", () => + { + Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); + }), + Runner.TestCase("NE succeeds", () => + { + Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); + }), + Runner.TestCase("EX succeeds", () => + { + Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); + }), + Runner.TestCase("NEX succeeds", () => + { + Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); + }) + }), + Runner.TestList("Query", new[] + { + Runner.TestCase("SelectFromTable succeeds", () => + { + Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", + "SELECT statement not correct"); + }), + Runner.TestCase("WhereById succeeds", () => + { + Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); + }), + Runner.TestList("WhereByField", new[] + { + Runner.TestCase("succeeds when a logical operator is passed", () => + { + Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test", + "WHERE clause not correct"); + }), + Runner.TestCase("succeeds when an existence operator is passed", () => + { + Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL", + "WHERE clause not correct"); + }) + }), + Runner.TestList("Definition", new[] + { + Runner.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"); + }), + Runner.TestList("EnsureKey", new[] + { + Runner.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"); + }), + Runner.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"); + }) + }), + Runner.TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("test.table", "gibberish", + new[] { "taco", "guac DESC", "salsa ASC" }), + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + "CREATE INDEX for multiple field statement incorrect"); + }) + }), + Runner.TestCase("Insert succeeds", () => + { + Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + }), + Runner.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"); + }), + Runner.TestList("Count", new[] + { + Runner.TestCase("All succeeds", () => + { + Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", + "Count query not correct"); + }), + Runner.TestCase("ByField succeeds", () => + { + Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ), + "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", + "JSON field text comparison count query not correct"); + }) + }), + Runner.TestList("Exists", new[] + { + Runner.TestCase("ById succeeds", () => + { + Expect.equal(Query.Exists.ById("tbl"), + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it", + "ID existence query not correct"); + }), + Runner.TestCase("ByField succeeds", () => + { + Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT), + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", + "JSON field text comparison exists query not correct"); + }) + }), + Runner.TestList("Find", new[] + { + Runner.TestCase("ById succeeds", () => + { + Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", + "SELECT by ID query not correct"); + }), + Runner.TestCase("ByField succeeds", () => + { + Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE), + "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", + "SELECT by JSON comparison query not correct"); + }) + }), + Runner.TestList("Update", new[] + { + Runner.TestCase("Full succeeds", () => + { + Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", + "UPDATE full statement not correct"); + }), + Runner.TestCase("PartialById succeeds", () => + { + Expect.equal(Query.Update.PartialById("tbl"), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); + }), + Runner.TestCase("PartialByField succeeds", () => + { + Expect.equal(Query.Update.PartialByField("tbl", "Part", Op.NE), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", + "UPDATE partial by JSON comparison query not correct"); + }) + }), + Runner.TestList("Delete", new[] + { + Runner.TestCase("ById succeeds", () => + { + Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", + "DELETE by ID query not correct"); + }), + Runner.TestCase("ByField succeeds", () => + { + Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX), + "DELETE FROM tbl WHERE data ->> 'gone' IS NULL", + "DELETE by JSON comparison query not correct"); + }) + }) + }) + }); +} diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs new file mode 100644 index 0000000..7c5d0fc --- /dev/null +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -0,0 +1,939 @@ +using Microsoft.Data.Sqlite; +using Microsoft.FSharp.Core; + +namespace BitBadger.Documents.Tests.CSharp; + +using Expecto.CSharp; +using Expecto; +using static Sqlite; + +/// +/// C# tests for the SQLite implementation of BitBadger.Documents +/// +public static class SqliteCSharpTests +{ + private static readonly List Documents = new() + { + 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 } + }; + + private static async Task LoadDocs() + { + foreach (var doc in Documents) await Insert(SqliteDb.TableName, doc); + } + + [Tests] + public static Test Integration = + Runner.TestList("Sqlite.C# Integration", new[] + { + Runner.TestCase("Configuration.UseConnectionString succeeds", () => + { + try + { + Configuration.UseConnectionString("Data Source=test.db"); + Expect.equal(Configuration.connectionString, + new FSharpOption("Data Source=test.db;Foreign Keys=True"), + "Connection string incorrect"); + } + finally + { + Configuration.UseConnectionString("Data Source=:memory:"); + } + }), + Runner.TestList("Definition", new[] + { + Runner.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) + { + var result = await Custom.Scalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + new SqliteParameter[] { new("@name", name) }, + rdr => rdr.GetInt64(0)); + return result > 0L; + } + }) + }), + Runner.TestList("Insert", new[] + { + Runner.TestCase("succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should be no documents in the table"); + await Insert(SqliteDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await Find.All(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + Runner.TestCase("fails for duplicate key", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + try + { + await Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); + } + catch (Exception) + { + // This is what is supposed to happen + } + }) + }), + Runner.TestList("Save", new[] + { + Runner.TestCase("succeeds when a document is inserted", async () => + { + await using var db = await SqliteDb.BuildDb(); + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should be no documents in the table"); + + await Save(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await Find.All(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + Runner.TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Insert(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await Find.ById(SqliteDb.TableName, "test"); + if (before is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.equal(before!.Id, "test", "The document is not correct"); + Expect.isNotNull(before.Sub, "There should have been a sub-document"); + Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); + Expect.equal(before.Sub.Bar, "b", "The document is not correct"); + + await Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + var after = await Find.ById(SqliteDb.TableName, "test"); + if (after is null) Expect.isTrue(false, "There should have been a document returned post-update"); + 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"); + }) + }), + Runner.TestList("Count", new[] + { + Runner.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"); + }), + Runner.TestCase("ByField succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + }) + }), + Runner.TestList("Exists", new[] + { + Runner.TestList("ById", new[] + { + Runner.TestCase("succeeds when a document exists", async () => + { + 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"); + }), + Runner.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"); + }) + }), + Runner.TestList("ByField", new[] + { + Runner.TestCase("succeeds when documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByField(SqliteDb.TableName, "NumValue", Op.GE, 10); + Expect.isTrue(exists, "There should have been existing documents"); + }), + Runner.TestCase("succeeds when no matching documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }) + }), + // testList "Find" [ + // testList "All" [ + // testTask "succeeds when there is data" { + // use! db = Db.buildDb () + // + // do! Insert(Db.tableName, JsonDocument(Id = "one", Value = "two")) + // do! Insert(Db.tableName, JsonDocument(Id = "three", Value = "four")) + // do! Insert(Db.tableName, JsonDocument(Id = "five", Value = "six")) + // + // let! results = Find.All Db.tableName + // Expect.hasCountOf results 3u isTrue "There should have been 3 documents returned" + // } + // testTask "succeeds when there is no data" { + // use! db = Db.buildDb () + // let! results = Find.All Db.tableName + // Expect.hasCountOf results 0u isTrue "There should have been no documents returned" + // } + // ] + // testList "ById" [ + // testTask "succeeds when a document is found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = Find.ById(Db.tableName, "two") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" + // } + // testTask "succeeds when a document is not found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = Find.ById(Db.tableName, "three hundred eighty-seven") + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // testList "ByField" [ + // testTask "succeeds when documents are found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! docs = Find.ByField(Db.tableName, "NumValue", Op.GT, 15) + // Expect.hasCountOf docs 2u isTrue "There should have been two documents returned" + // } + // testTask "succeeds when documents are not found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! docs = Find.ByField(Db.tableName, "Value", Op.EQ, "mauve") + // Expect.hasCountOf docs 0u isTrue "There should have been no documents returned" + // } + // ] + // testList "FirstByField" [ + // testTask "succeeds when a document is found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = Find.FirstByField(Db.tableName, "Value", Op.EQ, "another") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" + // } + // testTask "succeeds when multiple documents are found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = Find.FirstByField(Db.tableName, "Sub.Foo", Op.EQ, "green") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.contains [ "two"; "four" ] (doc :> JsonDocument).Id "An incorrect document was returned" + // } + // testTask "succeeds when a document is not found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = Find.FirstByField(Db.tableName, "Value", Op.EQ, "absent") + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // ] + // testList "Update" [ + // testList "Full" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let testDoc = JsonDocument(Id = "one", Sub = Some (SubDocument(Foo = "blue", Bar = "red"))) + // do! Update.Full(Db.tableName, "one", testDoc) + // let! after = Find.ById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // Expect.equal after.Id "one" "The updated document is not correct" + // Expect.isSome after.Sub "The updated document should have had a sub-document" + // Expect.equal after.Sub.Value.Foo "blue" "The updated sub-document is not correct" + // Expect.equal after.Sub.Value.Bar "red" "The updated sub-document is not correct" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // + // let! before = Find.All Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! Update.Full( + // Db.tableName, + // "test", + // JsonDocument(Id = "x", Sub = Some (SubDocument(Foo = "blue", Bar = "red")))) + // } + // ] + // testList "FullFunc" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Update.FullFunc( + // Db.tableName, + // System.Func _.Id, + // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) + // let! after = Find.ById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // 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.isNone after.Sub "The updated document should not have a sub-document" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // + // let! before = Find.All Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! Update.FullFunc( + // Db.tableName, + // System.Func _.Id, + // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) + // } + // ] + // testList "PartialById" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Update.PartialById(Db.tableName, "one", {| NumValue = 44 |}) + // let! after = Find.ById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // Expect.equal after.Id "one" "The updated document is not correct" + // Expect.equal after.NumValue 44 "The updated document is not correct" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // + // let! before = Find.All Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! Update.PartialById(Db.tableName, "test", {| Foo = "green" |}) + // } + // ] + // testList "PartialByField" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Update.PartialByField(Db.tableName, "Value", Op.EQ, "purple", {| NumValue = 77 |}) + // let! after = Count.ByField(Db.tableName, "NumValue", Op.EQ, 77) + // Expect.equal after 2L "There should have been 2 documents returned" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // + // let! before = Find.All Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! Update.PartialByField(Db.tableName, "Value", Op.EQ, "burgundy", {| Foo = "green" |}) + // } + // ] + // ] + // testList "Delete" [ + // testList "ById" [ + // testTask "succeeds when a document is deleted" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Delete.ById(Db.tableName, "four") + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 4L "There should have been 4 documents remaining" + // } + // testTask "succeeds when a document is not deleted" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Delete.ById(Db.tableName, "thirty") + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 5L "There should have been 5 documents remaining" + // } + // ] + // testList "ByField" [ + // testTask "succeeds when documents are deleted" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Delete.ByField(Db.tableName, "Value", Op.NE, "purple") + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 2L "There should have been 2 documents remaining" + // } + // testTask "succeeds when documents are not deleted" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Delete.ByField(Db.tableName, "Value", Op.EQ, "crimson") + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 5L "There should have been 5 documents remaining" + // } + // ] + // ] + // testList "Custom" [ + // testList "Single" [ + // testTask "succeeds when a row is found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = + // Custom.Single( + // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // [ SqliteParameter("@id", "one") ], + // FromData) + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "one" "The incorrect document was returned" + // } + // testTask "succeeds when a row is not found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! doc = + // Custom.Single( + // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // [ SqliteParameter("@id", "eighty") ], + // FromData) + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // testList "List" [ + // testTask "succeeds when data is found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! docs = Custom.List(Query.SelectFromTable Db.tableName, [], FromData) + // Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" + // } + // testTask "succeeds when data is not found" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // let! docs = + // Custom.List( + // $"SELECT data FROM {Db.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 = Db.buildDb () + // do! loadDocs () + // + // do! Custom.NonQuery($"DELETE FROM {Db.tableName}", []) + // + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 0L "There should be no documents remaining in the table" + // } + // testTask "succeeds when no data matches where clause" { + // use! db = Db.buildDb () + // do! loadDocs () + // + // do! Custom.NonQuery( + // $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", + // [ SqliteParameter("@value", 100) ]) + // + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 5L "There should be 5 documents remaining in the table" + // } + // ] + // testTask "Scalar succeeds" { + // use! db = Db.buildDb () + // + // let! nbr = Custom.Scalar("SELECT 5 AS test_value", [], System.Func _.GetInt32(0)) + // Expect.equal nbr 5 "The query should have returned the number 5" + // } + // ] + // testList "Extensions" [ + // testTask "EnsureTable succeeds" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let itExists (name: string) = task { + // let! result = + // conn.CustomScalar( + // $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it", + // [ SqliteParameter("@name", name) ], + // System.Func _.GetInt64(0)) + // return result > 0L + // } + // + // 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" + // } + // testList "Insert" [ + // testTask "succeeds" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" + // do! conn.Insert( + // Db.tableName, + // JsonDocument(Id = "turkey", Sub = Some (SubDocument(Foo = "gobble", Bar = "gobble")))) + // let! after = conn.FindAll Db.tableName + // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" + // } + // testTask "fails for duplicate key" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! conn.Insert(Db.tableName, JsonDocument(Id = "test")) + // Expect.throws + // (fun () -> + // conn.Insert(Db.tableName, JsonDocument(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 = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" + // + // do! conn.Save( + // Db.tableName, + // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) + // let! after = conn.FindAll Db.tableName + // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" + // } + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! conn.Insert( + // Db.tableName, + // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) + // + // let! before = conn.FindById(Db.tableName, "test") + // if isNull before then Expect.isTrue false "There should have been a document returned" + // let before = before :> JsonDocument + // Expect.equal before.Id "test" "The document is not correct" + // Expect.isSome before.Sub "There should have been a sub-document" + // Expect.equal before.Sub.Value.Foo "a" "The document is not correct" + // Expect.equal before.Sub.Value.Bar "b" "The document is not correct" + // + // do! Save(Db.tableName, JsonDocument(Id = "test")) + // let! after = conn.FindById(Db.tableName, "test") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // Expect.equal after.Id "test" "The updated document is not correct" + // Expect.isNone after.Sub "There should not have been a sub-document in the updated document" + // } + // ] + // testTask "CountAll succeeds" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! theCount = conn.CountAll Db.tableName + // Expect.equal theCount 5L "There should have been 5 matching documents" + // } + // testTask "CountByField succeeds" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! theCount = conn.CountByField(Db.tableName, "Value", Op.EQ, "purple") + // Expect.equal theCount 2L "There should have been 2 matching documents" + // } + // testList "ExistsById" [ + // testTask "succeeds when a document exists" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! exists = conn.ExistsById(Db.tableName, "three") + // Expect.isTrue exists "There should have been an existing document" + // } + // testTask "succeeds when a document does not exist" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! exists = conn.ExistsById(Db.tableName, "seven") + // Expect.isFalse exists "There should not have been an existing document" + // } + // ] + // testList "ExistsByField" [ + // testTask "succeeds when documents exist" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! exists = conn.ExistsByField(Db.tableName, "NumValue", Op.GE, 10) + // Expect.isTrue exists "There should have been existing documents" + // } + // testTask "succeeds when no matching documents exist" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! exists = conn.ExistsByField(Db.tableName, "Nothing", Op.EQ, "none") + // Expect.isFalse exists "There should not have been any existing documents" + // } + // ] + // testList "FindAll" [ + // testTask "succeeds when there is data" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // + // do! conn.Insert(Db.tableName, JsonDocument(Id = "one", Value = "two")) + // do! conn.Insert(Db.tableName, JsonDocument(Id = "three", Value = "four")) + // do! conn.Insert(Db.tableName, JsonDocument(Id = "five", Value = "six")) + // + // let! results = conn.FindAll Db.tableName + // Expect.hasCountOf results 3u isTrue "There should have been 3 documents returned" + // } + // testTask "succeeds when there is no data" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! results = conn.FindAll Db.tableName + // Expect.hasCountOf results 0u isTrue "There should have been no documents returned" + // } + // ] + // testList "FindById" [ + // testTask "succeeds when a document is found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = conn.FindById(Db.tableName, "two") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" + // } + // testTask "succeeds when a document is not found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = conn.FindById(Db.tableName, "three hundred eighty-seven") + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // testList "FindByField" [ + // testTask "succeeds when documents are found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! docs = conn.FindByField(Db.tableName, "NumValue", Op.GT, 15) + // Expect.hasCountOf docs 2u isTrue "There should have been two documents returned" + // } + // testTask "succeeds when documents are not found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! docs = conn.FindByField(Db.tableName, "Value", Op.EQ, "mauve") + // Expect.hasCountOf docs 0u isTrue "There should have been no documents returned" + // } + // ] + // testList "FindFirstByField" [ + // testTask "succeeds when a document is found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = conn.FindFirstByField(Db.tableName, "Value", Op.EQ, "another") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" + // } + // testTask "succeeds when multiple documents are found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = conn.FindFirstByField(Db.tableName, "Sub.Foo", Op.EQ, "green") + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.contains [ "two"; "four" ] (doc :> JsonDocument).Id "An incorrect document was returned" + // } + // testTask "succeeds when a document is not found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = conn.FindFirstByField(Db.tableName, "Value", Op.EQ, "absent") + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // testList "UpdateFull" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let testDoc = JsonDocument(Id = "one", Sub = Some (SubDocument(Foo = "blue", Bar = "red"))) + // do! conn.UpdateFull(Db.tableName, "one", testDoc) + // let! after = conn.FindById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // Expect.equal after.Id "one" "The updated document is not correct" + // Expect.isSome after.Sub "The updated document should have had a sub-document" + // Expect.equal after.Sub.Value.Foo "blue" "The updated sub-document is not correct" + // Expect.equal after.Sub.Value.Bar "red" "The updated sub-document is not correct" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! conn.UpdateFull( + // Db.tableName, + // "test", + // JsonDocument(Id = "x", Sub = Some (SubDocument(Foo = "blue", Bar = "red")))) + // } + // ] + // testList "UpdateFullFunc" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.UpdateFullFunc( + // Db.tableName, + // System.Func _.Id, + // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) + // let! after = conn.FindById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // 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.isNone after.Sub "The updated document should not have a sub-document" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! conn.UpdateFullFunc( + // Db.tableName, + // System.Func _.Id, + // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) + // } + // ] + // testList "UpdatePartialById" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.UpdatePartialById(Db.tableName, "one", {| NumValue = 44 |}) + // let! after = conn.FindById(Db.tableName, "one") + // if isNull after then Expect.isTrue false "There should have been a document returned post-update" + // let after = after :> JsonDocument + // Expect.equal after.Id "one" "The updated document is not correct" + // Expect.equal after.NumValue 44 "The updated document is not correct" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! conn.UpdatePartialById(Db.tableName, "test", {| Foo = "green" |}) + // } + // ] + // testList "UpdatePartialByField" [ + // testTask "succeeds when a document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.UpdatePartialByField(Db.tableName, "Value", Op.EQ, "purple", {| NumValue = 77 |}) + // let! after = conn.CountByField(Db.tableName, "NumValue", Op.EQ, 77) + // Expect.equal after 2L "There should have been 2 documents returned" + // } + // testTask "succeeds when no document is updated" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // let! before = conn.FindAll Db.tableName + // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + // + // // This not raising an exception is the test + // do! conn.UpdatePartialByField(Db.tableName, "Value", Op.EQ, "burgundy", {| Foo = "green" |}) + // } + // ] + // testList "DeleteById" [ + // testTask "succeeds when a document is deleted" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.DeleteById(Db.tableName, "four") + // let! remaining = conn.CountAll Db.tableName + // Expect.equal remaining 4L "There should have been 4 documents remaining" + // } + // testTask "succeeds when a document is not deleted" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.DeleteById(Db.tableName, "thirty") + // let! remaining = conn.CountAll Db.tableName + // Expect.equal remaining 5L "There should have been 5 documents remaining" + // } + // ] + // testList "DeleteByField" [ + // testTask "succeeds when documents are deleted" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.DeleteByField(Db.tableName, "Value", Op.NE, "purple") + // let! remaining = conn.CountAll Db.tableName + // Expect.equal remaining 2L "There should have been 2 documents remaining" + // } + // testTask "succeeds when documents are not deleted" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.DeleteByField(Db.tableName, "Value", Op.EQ, "crimson") + // let! remaining = Count.All Db.tableName + // Expect.equal remaining 5L "There should have been 5 documents remaining" + // } + // ] + // testList "CustomSingle" [ + // testTask "succeeds when a row is found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = + // conn.CustomSingle( + // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // [ SqliteParameter("@id", "one") ], + // FromData) + // if isNull doc then Expect.isTrue false "There should have been a document returned" + // Expect.equal (doc :> JsonDocument).Id "one" "The incorrect document was returned" + // } + // testTask "succeeds when a row is not found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! doc = + // conn.CustomSingle( + // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // [ SqliteParameter("@id", "eighty") ], + // FromData) + // Expect.isNull doc "There should not have been a document returned" + // } + // ] + // testList "CustomList" [ + // testTask "succeeds when data is found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! docs = conn.CustomList(Query.SelectFromTable Db.tableName, [], FromData) + // Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" + // } + // testTask "succeeds when data is not found" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // let! docs = + // conn.CustomList( + // $"SELECT data FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", + // [ SqliteParameter("@value", 100) ], + // FromData) + // Expect.isEmpty docs "There should have been no documents returned" + // } + // ] + // testList "CustomNonQuery" [ + // testTask "succeeds when operating on data" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.CustomNonQuery($"DELETE FROM {Db.tableName}", []) + // + // let! remaining = conn.CountAll Db.tableName + // Expect.equal remaining 0L "There should be no documents remaining in the table" + // } + // testTask "succeeds when no data matches where clause" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // do! loadDocs () + // + // do! conn.CustomNonQuery( + // $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", + // [ SqliteParameter("@value", 100) ]) + // + // let! remaining = conn.CountAll Db.tableName + // Expect.equal remaining 5L "There should be 5 documents remaining in the table" + // } + // ] + // testTask "CustomScalar succeeds" { + // use! db = Db.buildDb () + // use conn = Configuration.DbConn() + // + // let! nbr = + // conn.CustomScalar("SELECT 5 AS test_value", [], System.Func _.GetInt32(0)) + // Expect.equal nbr 5 "The query should have returned the number 5" + // } + // ] + // test "clean up database" { + // Configuration.UseConnectionString "data source=:memory:" + // } + // } + // ] + // |> testSequenced) + }); +} diff --git a/src/Tests.CSharp/SqliteDb.cs b/src/Tests.CSharp/SqliteDb.cs new file mode 100644 index 0000000..a34adc0 --- /dev/null +++ b/src/Tests.CSharp/SqliteDb.cs @@ -0,0 +1,59 @@ +namespace BitBadger.Documents.Tests; + +using System; +using System.IO; +using System.Threading.Tasks; +using static Sqlite; + +/// +/// A throwaway SQLite database file, which will be deleted when it goes out of scope +/// +public class ThrowawaySqliteDb : IDisposable, IAsyncDisposable +{ + private readonly string _dbName; + + public ThrowawaySqliteDb(string dbName) + { + _dbName = dbName; + } + + public void Dispose() + { + if (File.Exists(_dbName)) File.Delete(_dbName); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + if (File.Exists(_dbName)) File.Delete(_dbName); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } +} + +/// +/// Utility functions for dealing with SQLite databases +/// +public static class SqliteDb +{ + /// + /// The table name for the catalog metadata + /// + public static readonly string Catalog = "sqlite_master"; + + /// + /// The name of the table used for testing + /// + public static readonly string TableName = "test_table"; + + /// + /// Create a throwaway database file with the test_table defined + /// + public static async Task BuildDb() + { + var dbName = $"test-db-{Guid.NewGuid():n}.db"; + Configuration.UseConnectionString($"data source={dbName}"); + await Definition.EnsureTable(TableName); + return new ThrowawaySqliteDb(dbName); + } +} diff --git a/src/Tests.CSharp/Types.cs b/src/Tests.CSharp/Types.cs new file mode 100644 index 0000000..5e7f972 --- /dev/null +++ b/src/Tests.CSharp/Types.cs @@ -0,0 +1,15 @@ +namespace BitBadger.Documents.Tests.CSharp; + +public class SubDocument +{ + public string Foo { get; set; } = ""; + public string Bar { get; set; } = ""; +} + +public class JsonDocument +{ + public string Id { get; set; } = ""; + public string Value { get; set; } = ""; + public int NumValue { get; set; } = 0; + public SubDocument? Sub { get; set; } = null; +} diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index d6f064a..9dae8da 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -17,6 +17,7 @@ + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index fff4000..5dab352 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -29,10 +29,10 @@ let all = Expect.equal (string NE) "<>" "The not equal to operator was not correct" } test "EX succeeds" { - Expect.equal (string EX) "IS NOT NULL" """The "exists" operator ws not correct""" + 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 ws not correct""" + Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct""" } ] testList "Query" [ diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 4bd23f4..80749e8 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -1,6 +1,13 @@ open Expecto +open BitBadger.Documents.Tests.CSharp -let allTests = testList "BitBadger.Documents" [ CommonTests.all; SqliteTests.all ] +let allTests = + testList + "BitBadger.Documents" + [ CommonTests.all + CommonCSharpTests.Unit + SqliteTests.all + testSequenced SqliteCSharpTests.Integration ] [] let main args = runTestsWithCLIArgs [] args allTests -- 2.45.1 From b2cfcfdd63114be4ad25697f7a593ebf337fe339 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 25 Dec 2023 12:53:39 -0500 Subject: [PATCH 06/26] WIP on C# test migration --- src/Sqlite/Library.fs | 5 + src/Tests.CSharp/SqliteCSharpTests.cs | 937 +++++++++++++------------- 2 files changed, 492 insertions(+), 450 deletions(-) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 1ef745f..25dedcd 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -60,6 +60,11 @@ module Parameters = let fieldParam (value: obj) = SqliteParameter("@field", value) + /// An empty parameter sequence + [] + let noParams = + Seq.empty + /// Helper functions for handling results [] diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 7c5d0fc..c0f9873 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -1,10 +1,11 @@ -using Microsoft.Data.Sqlite; +using Expecto.CSharp; +using Expecto; +using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; +using Docs = BitBadger.Documents; namespace BitBadger.Documents.Tests.CSharp; -using Expecto.CSharp; -using Expecto; using static Sqlite; /// @@ -12,7 +13,7 @@ using static Sqlite; /// public static class SqliteCSharpTests { - private static readonly List Documents = new() + private static readonly List TestDocuments = new() { new() { Id = "one", Value = "FIRST!", NumValue = 0 }, new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, @@ -23,7 +24,7 @@ public static class SqliteCSharpTests private static async Task LoadDocs() { - foreach (var doc in Documents) await Insert(SqliteDb.TableName, doc); + foreach (var doc in TestDocuments) await Insert(SqliteDb.TableName, doc); } [Tests] @@ -44,6 +45,87 @@ public static class SqliteCSharpTests Configuration.UseConnectionString("Data Source=:memory:"); } }), + Runner.TestList("Custom", new[] + { + Runner.TestList("Single", new [] + { + Runner.TestCase("succeeds when a row is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Custom.Single( + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", + new[] { Parameters.Id("one") }, Results.FromData); + if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.equal(doc!.Id, "one", "The incorrect document was returned"); + }), + Runner.TestCase("succeeds when a row is not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + 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"); + }) + }), + Runner.TestList("List", new[] + { + Runner.TestCase("succeeds when data is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Custom.List(Docs.Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); + }), + Runner.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[] { new SqliteParameter("@value", 100) }, Results.FromData); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + Runner.TestList("NonQuery", new[] + { + Runner.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"); + }), + Runner.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"); + }) + }), + Runner.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"); + }) + }), Runner.TestList("Definition", new[] { Runner.TestCase("EnsureTable succeeds", async () => @@ -193,299 +275,254 @@ public static class SqliteCSharpTests }) }) }), - // testList "Find" [ - // testList "All" [ - // testTask "succeeds when there is data" { - // use! db = Db.buildDb () - // - // do! Insert(Db.tableName, JsonDocument(Id = "one", Value = "two")) - // do! Insert(Db.tableName, JsonDocument(Id = "three", Value = "four")) - // do! Insert(Db.tableName, JsonDocument(Id = "five", Value = "six")) - // - // let! results = Find.All Db.tableName - // Expect.hasCountOf results 3u isTrue "There should have been 3 documents returned" - // } - // testTask "succeeds when there is no data" { - // use! db = Db.buildDb () - // let! results = Find.All Db.tableName - // Expect.hasCountOf results 0u isTrue "There should have been no documents returned" - // } - // ] - // testList "ById" [ - // testTask "succeeds when a document is found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = Find.ById(Db.tableName, "two") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" - // } - // testTask "succeeds when a document is not found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = Find.ById(Db.tableName, "three hundred eighty-seven") - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // testList "ByField" [ - // testTask "succeeds when documents are found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! docs = Find.ByField(Db.tableName, "NumValue", Op.GT, 15) - // Expect.hasCountOf docs 2u isTrue "There should have been two documents returned" - // } - // testTask "succeeds when documents are not found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! docs = Find.ByField(Db.tableName, "Value", Op.EQ, "mauve") - // Expect.hasCountOf docs 0u isTrue "There should have been no documents returned" - // } - // ] - // testList "FirstByField" [ - // testTask "succeeds when a document is found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = Find.FirstByField(Db.tableName, "Value", Op.EQ, "another") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" - // } - // testTask "succeeds when multiple documents are found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = Find.FirstByField(Db.tableName, "Sub.Foo", Op.EQ, "green") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.contains [ "two"; "four" ] (doc :> JsonDocument).Id "An incorrect document was returned" - // } - // testTask "succeeds when a document is not found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = Find.FirstByField(Db.tableName, "Value", Op.EQ, "absent") - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // ] - // testList "Update" [ - // testList "Full" [ - // testTask "succeeds when a document is updated" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let testDoc = JsonDocument(Id = "one", Sub = Some (SubDocument(Foo = "blue", Bar = "red"))) - // do! Update.Full(Db.tableName, "one", testDoc) - // let! after = Find.ById(Db.tableName, "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // Expect.equal after.Id "one" "The updated document is not correct" - // Expect.isSome after.Sub "The updated document should have had a sub-document" - // Expect.equal after.Sub.Value.Foo "blue" "The updated sub-document is not correct" - // Expect.equal after.Sub.Value.Bar "red" "The updated sub-document is not correct" - // } - // testTask "succeeds when no document is updated" { - // use! db = Db.buildDb () - // - // let! before = Find.All Db.tableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // do! Update.Full( - // Db.tableName, - // "test", - // JsonDocument(Id = "x", Sub = Some (SubDocument(Foo = "blue", Bar = "red")))) - // } - // ] - // testList "FullFunc" [ - // testTask "succeeds when a document is updated" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Update.FullFunc( - // Db.tableName, - // System.Func _.Id, - // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) - // let! after = Find.ById(Db.tableName, "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // 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.isNone after.Sub "The updated document should not have a sub-document" - // } - // testTask "succeeds when no document is updated" { - // use! db = Db.buildDb () - // - // let! before = Find.All Db.tableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // do! Update.FullFunc( - // Db.tableName, - // System.Func _.Id, - // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) - // } - // ] - // testList "PartialById" [ - // testTask "succeeds when a document is updated" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Update.PartialById(Db.tableName, "one", {| NumValue = 44 |}) - // let! after = Find.ById(Db.tableName, "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // Expect.equal after.Id "one" "The updated document is not correct" - // Expect.equal after.NumValue 44 "The updated document is not correct" - // } - // testTask "succeeds when no document is updated" { - // use! db = Db.buildDb () - // - // let! before = Find.All Db.tableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // do! Update.PartialById(Db.tableName, "test", {| Foo = "green" |}) - // } - // ] - // testList "PartialByField" [ - // testTask "succeeds when a document is updated" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Update.PartialByField(Db.tableName, "Value", Op.EQ, "purple", {| NumValue = 77 |}) - // let! after = Count.ByField(Db.tableName, "NumValue", Op.EQ, 77) - // Expect.equal after 2L "There should have been 2 documents returned" - // } - // testTask "succeeds when no document is updated" { - // use! db = Db.buildDb () - // - // let! before = Find.All Db.tableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // do! Update.PartialByField(Db.tableName, "Value", Op.EQ, "burgundy", {| Foo = "green" |}) - // } - // ] - // ] - // testList "Delete" [ - // testList "ById" [ - // testTask "succeeds when a document is deleted" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Delete.ById(Db.tableName, "four") - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 4L "There should have been 4 documents remaining" - // } - // testTask "succeeds when a document is not deleted" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Delete.ById(Db.tableName, "thirty") - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 5L "There should have been 5 documents remaining" - // } - // ] - // testList "ByField" [ - // testTask "succeeds when documents are deleted" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Delete.ByField(Db.tableName, "Value", Op.NE, "purple") - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 2L "There should have been 2 documents remaining" - // } - // testTask "succeeds when documents are not deleted" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Delete.ByField(Db.tableName, "Value", Op.EQ, "crimson") - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 5L "There should have been 5 documents remaining" - // } - // ] - // ] - // testList "Custom" [ - // testList "Single" [ - // testTask "succeeds when a row is found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = - // Custom.Single( - // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", - // [ SqliteParameter("@id", "one") ], - // FromData) - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "one" "The incorrect document was returned" - // } - // testTask "succeeds when a row is not found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! doc = - // Custom.Single( - // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", - // [ SqliteParameter("@id", "eighty") ], - // FromData) - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // testList "List" [ - // testTask "succeeds when data is found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! docs = Custom.List(Query.SelectFromTable Db.tableName, [], FromData) - // Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" - // } - // testTask "succeeds when data is not found" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // let! docs = - // Custom.List( - // $"SELECT data FROM {Db.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 = Db.buildDb () - // do! loadDocs () - // - // do! Custom.NonQuery($"DELETE FROM {Db.tableName}", []) - // - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 0L "There should be no documents remaining in the table" - // } - // testTask "succeeds when no data matches where clause" { - // use! db = Db.buildDb () - // do! loadDocs () - // - // do! Custom.NonQuery( - // $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", - // [ SqliteParameter("@value", 100) ]) - // - // let! remaining = Count.All Db.tableName - // Expect.equal remaining 5L "There should be 5 documents remaining in the table" - // } - // ] - // testTask "Scalar succeeds" { - // use! db = Db.buildDb () - // - // let! nbr = Custom.Scalar("SELECT 5 AS test_value", [], System.Func _.GetInt32(0)) - // Expect.equal nbr 5 "The query should have returned the number 5" - // } - // ] - // testList "Extensions" [ - // testTask "EnsureTable succeeds" { + Runner.TestList("Find", new[] + { + Runner.TestList("All", new[] + { + Runner.TestCase("succeeds when there is data", async () => + { + await using var db = await SqliteDb.BuildDb(); + + await Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await 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"); + }), + Runner.TestCase("succeeds when there is no data", async () => + { + await using var db = await SqliteDb.BuildDb(); + var results = await Find.All(SqliteDb.TableName); + Expect.equal(results.Count, 0, "There should have been no documents returned"); + }) + }), + Runner.TestList("ById", new[] + { + Runner.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"); + if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + Runner.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"); + }) + }), + Runner.TestList("ByField", new[] + { + Runner.TestCase("succeeds when documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByField(SqliteDb.TableName, "NumValue", Op.GT, 15); + Expect.equal(docs.Count, 2, "There should have been two documents returned"); + }), + Runner.TestCase("succeeds when documents are not found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var docs = await Find.ByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); + Expect.equal(docs.Count, 0, "There should have been no documents returned"); + }) + }), + Runner.TestList("FirstByField", new[] + { + Runner.TestCase("succeeds when a document is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByField(SqliteDb.TableName, "Value", Op.EQ, "another"); + if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.equal(doc!.Id, "two", "The incorrect document was returned"); + }), + Runner.TestCase("succeeds when multiple documents are found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var doc = await Find.FirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, "green"); + if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); + }), + Runner.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, "Value", Op.EQ, "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }) + }), + Runner.TestList("Update", new[] + { + Runner.TestList("Full", new[] + { + Runner.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.Full(SqliteDb.TableName, "one", testDoc); + var after = await Find.ById(SqliteDb.TableName, "one"); + if (after is null) + Expect.isTrue(false, "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"); + }), + Runner.TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.Full(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + Runner.TestList("FullFunc", new[] + { + Runner.TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + if (after is null) + Expect.isTrue(false, "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"); + }), + Runner.TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + Runner.TestList("PartialById", new[] + { + Runner.TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Update.PartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await Find.ById(SqliteDb.TableName, "one"); + if (after is null) + Expect.isTrue(false, "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"); + }), + Runner.TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.PartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + }), + Runner.TestList("PartialByField", new[] + { + Runner.TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", + new { NumValue = 77 }); + var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 77); + Expect.equal(after, 2L, "There should have been 2 documents returned"); + }), + Runner.TestCase("succeeds when no document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + + var before = await Find.All(SqliteDb.TableName); + Expect.equal(before.Count, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); + }) + }) + }), + Runner.TestList("Delete", new[] + { + Runner.TestList("ById", new[] + { + Runner.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"); + }), + Runner.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"); + }) + }), + Runner.TestList("ByField", new[] + { + Runner.TestCase("succeeds when documents are deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByField(SqliteDb.TableName, "Value", Op.NE, "purple"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); + }), + Runner.TestCase("succeeds when documents are not deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await Delete.ByField(SqliteDb.TableName, "Value", Op.EQ, "crimson"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + }) + }), + // Runner.TestList("Extensions" [ + // Runner.TestCase("EnsureTable succeeds" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() // let itExists (name: string) = task { @@ -502,57 +539,57 @@ public static class SqliteCSharpTests // Expect.isFalse exists "The table should not exist already" // Expect.isFalse alsoExists "The key index should not exist already" // - // do! Definition.EnsureTable "ensured" + // await 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" // } - // testList "Insert" [ - // testTask "succeeds" { + // Runner.TestList("Insert" [ + // Runner.TestCase("succeeds" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" - // do! conn.Insert( - // Db.tableName, + // await conn.Insert( + // SqliteDb.TableName , // JsonDocument(Id = "turkey", Sub = Some (SubDocument(Foo = "gobble", Bar = "gobble")))) - // let! after = conn.FindAll Db.tableName + // let! after = conn.FindAll SqliteDb.TableName // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" // } - // testTask "fails for duplicate key" { + // Runner.TestCase("fails for duplicate key" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! conn.Insert(Db.tableName, JsonDocument(Id = "test")) + // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "test")) // Expect.throws // (fun () -> - // conn.Insert(Db.tableName, JsonDocument(Id = "test")) + // conn.Insert(SqliteDb.TableName , JsonDocument(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" { + // Runner.TestList("Save" [ + // Runner.TestCase("succeeds when a document is inserted" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" // - // do! conn.Save( - // Db.tableName, + // await conn.Save( + // SqliteDb.TableName , // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) - // let! after = conn.FindAll Db.tableName + // let! after = conn.FindAll SqliteDb.TableName // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" // } - // testTask "succeeds when a document is updated" { + // Runner.TestCase("succeeds when a document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! conn.Insert( - // Db.tableName, + // await conn.Insert( + // SqliteDb.TableName , // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) // - // let! before = conn.FindById(Db.tableName, "test") + // let! before = conn.FindById(SqliteDb.TableName , "test") // if isNull before then Expect.isTrue false "There should have been a document returned" // let before = before :> JsonDocument // Expect.equal before.Id "test" "The document is not correct" @@ -560,159 +597,159 @@ public static class SqliteCSharpTests // Expect.equal before.Sub.Value.Foo "a" "The document is not correct" // Expect.equal before.Sub.Value.Bar "b" "The document is not correct" // - // do! Save(Db.tableName, JsonDocument(Id = "test")) - // let! after = conn.FindById(Db.tableName, "test") + // await Save(SqliteDb.TableName , JsonDocument(Id = "test")) + // let! after = conn.FindById(SqliteDb.TableName , "test") // if isNull after then Expect.isTrue false "There should have been a document returned post-update" // let after = after :> JsonDocument // Expect.equal after.Id "test" "The updated document is not correct" // Expect.isNone after.Sub "There should not have been a sub-document in the updated document" // } // ] - // testTask "CountAll succeeds" { + // Runner.TestCase("CountAll succeeds" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! theCount = conn.CountAll Db.tableName + // let! theCount = conn.CountAll SqliteDb.TableName // Expect.equal theCount 5L "There should have been 5 matching documents" // } - // testTask "CountByField succeeds" { + // Runner.TestCase("CountByField succeeds" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! theCount = conn.CountByField(Db.tableName, "Value", Op.EQ, "purple") + // let! theCount = conn.CountByField(SqliteDb.TableName , "Value", Op.EQ, "purple") // Expect.equal theCount 2L "There should have been 2 matching documents" // } - // testList "ExistsById" [ - // testTask "succeeds when a document exists" { + // Runner.TestList("ExistsById" [ + // Runner.TestCase("succeeds when a document exists" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! exists = conn.ExistsById(Db.tableName, "three") + // let! exists = conn.ExistsById(SqliteDb.TableName , "three") // Expect.isTrue exists "There should have been an existing document" // } - // testTask "succeeds when a document does not exist" { + // Runner.TestCase("succeeds when a document does not exist" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! exists = conn.ExistsById(Db.tableName, "seven") + // let! exists = conn.ExistsById(SqliteDb.TableName , "seven") // Expect.isFalse exists "There should not have been an existing document" // } // ] - // testList "ExistsByField" [ - // testTask "succeeds when documents exist" { + // Runner.TestList("ExistsByField" [ + // Runner.TestCase("succeeds when documents exist" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! exists = conn.ExistsByField(Db.tableName, "NumValue", Op.GE, 10) + // let! exists = conn.ExistsByField(SqliteDb.TableName , "NumValue", Op.GE, 10) // Expect.isTrue exists "There should have been existing documents" // } - // testTask "succeeds when no matching documents exist" { + // Runner.TestCase("succeeds when no matching documents exist" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! exists = conn.ExistsByField(Db.tableName, "Nothing", Op.EQ, "none") + // let! exists = conn.ExistsByField(SqliteDb.TableName , "Nothing", Op.EQ, "none") // Expect.isFalse exists "There should not have been any existing documents" // } // ] - // testList "FindAll" [ - // testTask "succeeds when there is data" { + // Runner.TestList("FindAll" [ + // Runner.TestCase("succeeds when there is data" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() // - // do! conn.Insert(Db.tableName, JsonDocument(Id = "one", Value = "two")) - // do! conn.Insert(Db.tableName, JsonDocument(Id = "three", Value = "four")) - // do! conn.Insert(Db.tableName, JsonDocument(Id = "five", Value = "six")) + // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "one", Value = "two")) + // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "three", Value = "four")) + // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "five", Value = "six")) // - // let! results = conn.FindAll Db.tableName + // let! results = conn.FindAll SqliteDb.TableName // Expect.hasCountOf results 3u isTrue "There should have been 3 documents returned" // } - // testTask "succeeds when there is no data" { + // Runner.TestCase("succeeds when there is no data" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! results = conn.FindAll Db.tableName + // let! results = conn.FindAll SqliteDb.TableName // Expect.hasCountOf results 0u isTrue "There should have been no documents returned" // } // ] - // testList "FindById" [ - // testTask "succeeds when a document is found" { + // Runner.TestList("FindById" [ + // Runner.TestCase("succeeds when a document is found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! doc = conn.FindById(Db.tableName, "two") + // let! doc = conn.FindById(SqliteDb.TableName , "two") // if isNull doc then Expect.isTrue false "There should have been a document returned" // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" // } - // testTask "succeeds when a document is not found" { + // Runner.TestCase("succeeds when a document is not found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! doc = conn.FindById(Db.tableName, "three hundred eighty-seven") + // let! doc = conn.FindById(SqliteDb.TableName , "three hundred eighty-seven") // Expect.isNull doc "There should not have been a document returned" // } // ] - // testList "FindByField" [ - // testTask "succeeds when documents are found" { + // Runner.TestList("FindByField" [ + // Runner.TestCase("succeeds when documents are found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! docs = conn.FindByField(Db.tableName, "NumValue", Op.GT, 15) + // let! docs = conn.FindByField(SqliteDb.TableName , "NumValue", Op.GT, 15) // Expect.hasCountOf docs 2u isTrue "There should have been two documents returned" // } - // testTask "succeeds when documents are not found" { + // Runner.TestCase("succeeds when documents are not found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! docs = conn.FindByField(Db.tableName, "Value", Op.EQ, "mauve") + // let! docs = conn.FindByField(SqliteDb.TableName , "Value", Op.EQ, "mauve") // Expect.hasCountOf docs 0u isTrue "There should have been no documents returned" // } // ] - // testList "FindFirstByField" [ - // testTask "succeeds when a document is found" { + // Runner.TestList("FindFirstByField" [ + // Runner.TestCase("succeeds when a document is found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! doc = conn.FindFirstByField(Db.tableName, "Value", Op.EQ, "another") + // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Value", Op.EQ, "another") // if isNull doc then Expect.isTrue false "There should have been a document returned" // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" // } - // testTask "succeeds when multiple documents are found" { + // Runner.TestCase("succeeds when multiple documents are found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! doc = conn.FindFirstByField(Db.tableName, "Sub.Foo", Op.EQ, "green") + // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Sub.Foo", Op.EQ, "green") // if isNull doc then Expect.isTrue false "There should have been a document returned" // Expect.contains [ "two"; "four" ] (doc :> JsonDocument).Id "An incorrect document was returned" // } - // testTask "succeeds when a document is not found" { + // Runner.TestCase("succeeds when a document is not found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! doc = conn.FindFirstByField(Db.tableName, "Value", Op.EQ, "absent") + // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Value", Op.EQ, "absent") // Expect.isNull doc "There should not have been a document returned" // } // ] - // testList "UpdateFull" [ - // testTask "succeeds when a document is updated" { + // Runner.TestList("UpdateFull" [ + // Runner.TestCase("succeeds when a document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // // let testDoc = JsonDocument(Id = "one", Sub = Some (SubDocument(Foo = "blue", Bar = "red"))) - // do! conn.UpdateFull(Db.tableName, "one", testDoc) - // let! after = conn.FindById(Db.tableName, "one") + // await conn.UpdateFull(SqliteDb.TableName , "one", testDoc) + // let! after = conn.FindById(SqliteDb.TableName , "one") // if isNull after then Expect.isTrue false "There should have been a document returned post-update" // let after = after :> JsonDocument // Expect.equal after.Id "one" "The updated document is not correct" @@ -720,30 +757,30 @@ public static class SqliteCSharpTests // Expect.equal after.Sub.Value.Foo "blue" "The updated sub-document is not correct" // Expect.equal after.Sub.Value.Bar "red" "The updated sub-document is not correct" // } - // testTask "succeeds when no document is updated" { + // Runner.TestCase("succeeds when no document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // // // This not raising an exception is the test - // do! conn.UpdateFull( - // Db.tableName, + // await conn.UpdateFull( + // SqliteDb.TableName , // "test", // JsonDocument(Id = "x", Sub = Some (SubDocument(Foo = "blue", Bar = "red")))) // } // ] - // testList "UpdateFullFunc" [ - // testTask "succeeds when a document is updated" { + // Runner.TestList("UpdateFullFunc" [ + // Runner.TestCase("succeeds when a document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.UpdateFullFunc( - // Db.tableName, + // await conn.UpdateFullFunc( + // SqliteDb.TableName , // System.Func _.Id, // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) - // let! after = conn.FindById(Db.tableName, "one") + // let! after = conn.FindById(SqliteDb.TableName , "one") // if isNull after then Expect.isTrue false "There should have been a document returned post-update" // let after = after :> JsonDocument // Expect.equal after.Id "one" "The updated document is incorrect" @@ -751,176 +788,176 @@ public static class SqliteCSharpTests // Expect.equal after.NumValue 1 "The updated document is incorrect" // Expect.isNone after.Sub "The updated document should not have a sub-document" // } - // testTask "succeeds when no document is updated" { + // Runner.TestCase("succeeds when no document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // // // This not raising an exception is the test - // do! conn.UpdateFullFunc( - // Db.tableName, + // await conn.UpdateFullFunc( + // SqliteDb.TableName , // System.Func _.Id, // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) // } // ] - // testList "UpdatePartialById" [ - // testTask "succeeds when a document is updated" { + // Runner.TestList("UpdatePartialById" [ + // Runner.TestCase("succeeds when a document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.UpdatePartialById(Db.tableName, "one", {| NumValue = 44 |}) - // let! after = conn.FindById(Db.tableName, "one") + // await conn.UpdatePartialById(SqliteDb.TableName , "one", {| NumValue = 44 |}) + // let! after = conn.FindById(SqliteDb.TableName , "one") // if isNull after then Expect.isTrue false "There should have been a document returned post-update" // let after = after :> JsonDocument // Expect.equal after.Id "one" "The updated document is not correct" // Expect.equal after.NumValue 44 "The updated document is not correct" // } - // testTask "succeeds when no document is updated" { + // Runner.TestCase("succeeds when no document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // // // This not raising an exception is the test - // do! conn.UpdatePartialById(Db.tableName, "test", {| Foo = "green" |}) + // await conn.UpdatePartialById(SqliteDb.TableName , "test", {| Foo = "green" |}) // } // ] - // testList "UpdatePartialByField" [ - // testTask "succeeds when a document is updated" { + // Runner.TestList("UpdatePartialByField" [ + // Runner.TestCase("succeeds when a document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.UpdatePartialByField(Db.tableName, "Value", Op.EQ, "purple", {| NumValue = 77 |}) - // let! after = conn.CountByField(Db.tableName, "NumValue", Op.EQ, 77) + // await conn.UpdatePartialByField(SqliteDb.TableName , "Value", Op.EQ, "purple", {| NumValue = 77 |}) + // let! after = conn.CountByField(SqliteDb.TableName , "NumValue", Op.EQ, 77) // Expect.equal after 2L "There should have been 2 documents returned" // } - // testTask "succeeds when no document is updated" { + // Runner.TestCase("succeeds when no document is updated" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // let! before = conn.FindAll Db.tableName + // let! before = conn.FindAll SqliteDb.TableName // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // // // This not raising an exception is the test - // do! conn.UpdatePartialByField(Db.tableName, "Value", Op.EQ, "burgundy", {| Foo = "green" |}) + // await conn.UpdatePartialByField(SqliteDb.TableName , "Value", Op.EQ, "burgundy", {| Foo = "green" |}) // } // ] - // testList "DeleteById" [ - // testTask "succeeds when a document is deleted" { + // Runner.TestList("DeleteById" [ + // Runner.TestCase("succeeds when a document is deleted" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.DeleteById(Db.tableName, "four") - // let! remaining = conn.CountAll Db.tableName + // await conn.DeleteById(SqliteDb.TableName , "four") + // let! remaining = conn.CountAll SqliteDb.TableName // Expect.equal remaining 4L "There should have been 4 documents remaining" // } - // testTask "succeeds when a document is not deleted" { + // Runner.TestCase("succeeds when a document is not deleted" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.DeleteById(Db.tableName, "thirty") - // let! remaining = conn.CountAll Db.tableName + // await conn.DeleteById(SqliteDb.TableName , "thirty") + // let! remaining = conn.CountAll SqliteDb.TableName // Expect.equal remaining 5L "There should have been 5 documents remaining" // } // ] - // testList "DeleteByField" [ - // testTask "succeeds when documents are deleted" { + // Runner.TestList("DeleteByField" [ + // Runner.TestCase("succeeds when documents are deleted" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.DeleteByField(Db.tableName, "Value", Op.NE, "purple") - // let! remaining = conn.CountAll Db.tableName + // await conn.DeleteByField(SqliteDb.TableName , "Value", Op.NE, "purple") + // let! remaining = conn.CountAll SqliteDb.TableName // Expect.equal remaining 2L "There should have been 2 documents remaining" // } - // testTask "succeeds when documents are not deleted" { + // Runner.TestCase("succeeds when documents are not deleted" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.DeleteByField(Db.tableName, "Value", Op.EQ, "crimson") - // let! remaining = Count.All Db.tableName + // await conn.DeleteByField(SqliteDb.TableName , "Value", Op.EQ, "crimson") + // let! remaining = Count.All SqliteDb.TableName // Expect.equal remaining 5L "There should have been 5 documents remaining" // } // ] - // testList "CustomSingle" [ - // testTask "succeeds when a row is found" { + // Runner.TestList("CustomSingle" [ + // Runner.TestCase("succeeds when a row is found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // // let! doc = // conn.CustomSingle( - // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'Id' = @id", // [ SqliteParameter("@id", "one") ], // FromData) // if isNull doc then Expect.isTrue false "There should have been a document returned" // Expect.equal (doc :> JsonDocument).Id "one" "The incorrect document was returned" // } - // testTask "succeeds when a row is not found" { + // Runner.TestCase("succeeds when a row is not found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // // let! doc = // conn.CustomSingle( - // $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id", + // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'Id' = @id", // [ SqliteParameter("@id", "eighty") ], // FromData) // Expect.isNull doc "There should not have been a document returned" // } // ] - // testList "CustomList" [ - // testTask "succeeds when data is found" { + // Runner.TestList("CustomList" [ + // Runner.TestCase("succeeds when data is found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // let! docs = conn.CustomList(Query.SelectFromTable Db.tableName, [], FromData) + // let! docs = conn.CustomList(Query.SelectFromTable SqliteDb.TableName , [], FromData) // Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" // } - // testTask "succeeds when data is not found" { + // Runner.TestCase("succeeds when data is not found" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // // let! docs = // conn.CustomList( - // $"SELECT data FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", + // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'NumValue' > @value", // [ SqliteParameter("@value", 100) ], // FromData) // Expect.isEmpty docs "There should have been no documents returned" // } // ] - // testList "CustomNonQuery" [ - // testTask "succeeds when operating on data" { + // Runner.TestList("CustomNonQuery" [ + // Runner.TestCase("succeeds when operating on data" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.CustomNonQuery($"DELETE FROM {Db.tableName}", []) + // await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName }", []) // - // let! remaining = conn.CountAll Db.tableName + // let! remaining = conn.CountAll SqliteDb.TableName // Expect.equal remaining 0L "There should be no documents remaining in the table" // } - // testTask "succeeds when no data matches where clause" { + // Runner.TestCase("succeeds when no data matches where clause" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() - // do! loadDocs () + // await LoadDocs(); // - // do! conn.CustomNonQuery( - // $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value", + // await conn.CustomNonQuery( + // $"DELETE FROM {SqliteDb.TableName } WHERE data ->> 'NumValue' > @value", // [ SqliteParameter("@value", 100) ]) // - // let! remaining = conn.CountAll Db.tableName + // let! remaining = conn.CountAll SqliteDb.TableName // Expect.equal remaining 5L "There should be 5 documents remaining in the table" // } // ] - // testTask "CustomScalar succeeds" { + // Runner.TestCase("CustomScalar succeeds" { // use! db = Db.buildDb () // use conn = Configuration.DbConn() // -- 2.45.1 From 6ce619ac606794cecad51e6d62508b5bfe6e5eb9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 25 Dec 2023 15:24:23 -0500 Subject: [PATCH 07/26] WIP on test integration --- src/Tests.CSharp/SqliteDb.cs | 4 +- src/Tests/SqliteTests.fs | 479 +++++++++++++++++------------------ 2 files changed, 238 insertions(+), 245 deletions(-) diff --git a/src/Tests.CSharp/SqliteDb.cs b/src/Tests.CSharp/SqliteDb.cs index a34adc0..876ab46 100644 --- a/src/Tests.CSharp/SqliteDb.cs +++ b/src/Tests.CSharp/SqliteDb.cs @@ -39,12 +39,12 @@ public static class SqliteDb /// /// The table name for the catalog metadata /// - public static readonly string Catalog = "sqlite_master"; + public const string Catalog = "sqlite_master"; /// /// The name of the table used for testing /// - public static readonly string TableName = "test_table"; + public const string TableName = "test_table"; /// /// Create a throwaway database file with the test_table defined diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 75644ed..56d925a 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -12,53 +12,44 @@ type JsonDocument = let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } - -open BitBadger.Documents.Sqlite - -/// Database access for these tests -module Db = - - open System - open System.IO - open System.Threading.Tasks - - /// The table name for the catalog metadata - let catalog = "sqlite_master" - - /// The name of the table used for testing - let tableName = "test_table" - - /// A throwaway SQLite database file, which will be deleted when it goes out of scope - type ThrowawaySqliteDb(dbName: string) = - let deleteMe () = - if File.Exists dbName then File.Delete dbName - interface IDisposable with - member _.Dispose() = - deleteMe () - interface IAsyncDisposable with - member _.DisposeAsync() = - deleteMe () - ValueTask.CompletedTask - - /// Create a throwaway database file with the test_table defined - let buildDb () = task { - let dbName = $"""test-db-{Guid.NewGuid().ToString("n")}.db""" - Configuration.useConnectionString $"data source={dbName}" - do! Definition.ensureTable tableName - return new ThrowawaySqliteDb(dbName) - } - - /// A function that always returns true let isTrue<'T> (_ : 'T) = true open BitBadger.Documents +open BitBadger.Documents.Sqlite +open BitBadger.Documents.Tests open Expecto open Microsoft.Data.Sqlite +/// Unit tests for the SQLite library +let unitTests = + testList "Unit" [ + 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" + } + test "fieldParam succeeds" { + let theParam = fieldParam 99 + Expect.equal theParam.ParameterName "@field" "The parameter name is incorrect" + Expect.equal theParam.Value 99 "The parameter value is incorrect" + } + test "noParams succeeds" { + Expect.isEmpty noParams "The parameter list should have been empty" + } + ] + // Results are exhaustively executed in the context of other tests + ] + /// These tests each use a fresh copy of a SQLite database -let all = +let integrationTests = let documents = [ { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } @@ -67,7 +58,7 @@ let all = { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } ] let loadDocs () = backgroundTask { - for doc in documents do do! insert Db.tableName doc + for doc in documents do do! insert SqliteDb.TableName doc } testList "Integration" [ testList "Configuration" [ @@ -110,11 +101,11 @@ let all = ] testList "Definition" [ testTask "ensureTable succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() let itExists (name: string) = task { let! result = Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it" + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" [ SqliteParameter("@name", name) ] _.GetInt64(0) return result > 0 @@ -134,97 +125,97 @@ let all = ] testList "insert" [ testTask "succeeds" { - use! db = Db.buildDb () - let! before = Find.all Db.tableName + 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 Db.tableName testDoc - let! after = Find.all Db.tableName + 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 = Db.buildDb () - do! insert Db.tableName { emptyDoc with Id = "test" } + use! db = SqliteDb.BuildDb() + do! insert SqliteDb.TableName { emptyDoc with Id = "test" } Expect.throws (fun () -> - insert Db.tableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) + 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 = Db.buildDb () - let! before = Find.all Db.tableName + 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 Db.tableName testDoc - let! after = Find.all Db.tableName + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! insert Db.tableName testDoc + do! insert SqliteDb.TableName testDoc - let! before = Find.byId Db.tableName "test" + let! before = Find.byId SqliteDb.TableName "test" if Option.isNone before then Expect.isTrue false "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 Db.tableName upd8Doc - let! after = Find.byId Db.tableName "test" + do! save SqliteDb.TableName upd8Doc + let! after = Find.byId SqliteDb.TableName "test" if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! theCount = Count.all Db.tableName + let! theCount = Count.all SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } testTask "byField succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! theCount = Count.byField Db.tableName "Value" EQ "purple" + let! theCount = Count.byField SqliteDb.TableName "Value" EQ "purple" Expect.equal theCount 2L "There should have been 2 matching documents" } ] testList "Exists" [ testList "byId" [ testTask "succeeds when a document exists" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byId Db.tableName "three" + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byId Db.tableName "seven" + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byField Db.tableName "NumValue" EQ 10 + let! exists = Exists.byField SqliteDb.TableName "NumValue" EQ 10 Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when no matching documents exist" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! exists = Exists.byField Db.tableName "Nothing" LT "none" + let! exists = Exists.byField SqliteDb.TableName "Nothing" LT "none" Expect.isFalse exists "There should not have been any existing documents" } ] @@ -232,13 +223,13 @@ let all = testList "Find" [ testList "all" [ testTask "succeeds when there is data" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() - do! insert Db.tableName { Foo = "one"; Bar = "two" } - do! insert Db.tableName { Foo = "three"; Bar = "four" } - do! insert Db.tableName { Foo = "five"; Bar = "six" } + 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 Db.tableName + let! results = Find.all SqliteDb.TableName let expected = [ { Foo = "one"; Bar = "two" } { Foo = "three"; Bar = "four" } @@ -247,66 +238,66 @@ let all = Expect.equal results expected "There should have been 3 documents returned" } testTask "succeeds when there is no data" { - use! db = Db.buildDb () - let! results = Find.all Db.tableName + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.byId Db.tableName "two" + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.byId Db.tableName "three hundred eighty-seven" + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! docs = Find.byField Db.tableName "NumValue" GT 15 + let! docs = Find.byField SqliteDb.TableName "NumValue" GT 15 Expect.equal (List.length docs) 2 "There should have been two documents returned" } testTask "succeeds when documents are not found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! docs = Find.byField Db.tableName "NumValue" GT 100 + let! docs = Find.byField SqliteDb.TableName "NumValue" GT 100 Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" } ] testList "firstByField" [ testTask "succeeds when a document is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField Db.tableName "Value" EQ "another" + let! doc = Find.firstByField SqliteDb.TableName "Value" EQ "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField Db.tableName "Sub.Foo" EQ "green" + let! doc = Find.firstByField SqliteDb.TableName "Sub.Foo" EQ "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! doc = Find.firstByField Db.tableName "Value" EQ "absent" + let! doc = Find.firstByField SqliteDb.TableName "Value" EQ "absent" Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] @@ -314,36 +305,36 @@ let all = testList "Update" [ testList "full" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! Update.full Db.tableName "one" testDoc - let! after = Find.byId Db.tableName "one" + do! Update.full SqliteDb.TableName "one" testDoc + let! after = Find.byId SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() - let! before = Find.all Db.tableName + let! before = Find.all SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test do! Update.full - Db.tableName + SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] testList "fullFunc" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.fullFunc Db.tableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId Db.tableName "one" + do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" Expect.equal @@ -352,90 +343,90 @@ let all = "The updated document is not correct" } testTask "succeeds when no document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() - let! before = Find.all Db.tableName + let! before = Find.all SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test - do! Update.fullFunc Db.tableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] testList "partialById" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.partialById Db.tableName "one" {| NumValue = 44 |} - let! after = Find.byId Db.tableName "one" + do! Update.partialById SqliteDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() - let! before = Find.all Db.tableName + let! before = Find.all SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialById Db.tableName "test" {| Foo = "green" |} + do! Update.partialById SqliteDb.TableName "test" {| Foo = "green" |} } ] testList "partialByField" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.partialByField Db.tableName "Value" EQ "purple" {| NumValue = 77 |} - let! after = Count.byField Db.tableName "NumValue" EQ 77 + do! Update.partialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = Count.byField SqliteDb.TableName "NumValue" EQ 77 Expect.equal after 2L "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() - let! before = Find.all Db.tableName + let! before = Find.all SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByField Db.tableName "Value" EQ "burgundy" {| Foo = "green" |} + do! Update.partialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] ] testList "Delete" [ testList "byId" [ testTask "succeeds when a document is deleted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byId Db.tableName "four" - let! remaining = Count.all Db.tableName + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byId Db.tableName "thirty" - let! remaining = Count.all Db.tableName + 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byField Db.tableName "Value" NE "purple" - let! remaining = Count.all Db.tableName + do! Delete.byField SqliteDb.TableName "Value" NE "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Delete.byField Db.tableName "Value" EQ "crimson" - let! remaining = Count.all Db.tableName + do! Delete.byField SqliteDb.TableName "Value" EQ "crimson" + let! remaining = Count.all SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } ] @@ -443,24 +434,24 @@ let all = testList "Custom" [ testList "single" [ testTask "succeeds when a row is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () let! doc = Custom.single - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + $"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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () let! doc = Custom.single - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" [ SqliteParameter("@id", "eighty") ] fromData Expect.isNone doc "There should not have been a document returned" @@ -468,19 +459,19 @@ let all = ] testList "list" [ testTask "succeeds when data is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - let! docs = Custom.list (Query.selectFromTable Db.tableName) [] fromData + let! docs = Custom.list (Query.selectFromTable SqliteDb.TableName) [] fromData Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" } testTask "succeeds when data is not found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () let! docs = Custom.list - $"SELECT data FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" [ SqliteParameter("@value", 100) ] fromData Expect.isEmpty docs "There should have been no documents returned" @@ -488,28 +479,28 @@ let all = ] testList "nonQuery" [ testTask "succeeds when operating on data" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () - do! Custom.nonQuery $"DELETE FROM {Db.tableName}" [] + do! Custom.nonQuery $"DELETE FROM {SqliteDb.TableName}" [] - let! remaining = Count.all Db.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 = Db.buildDb () + use! db = SqliteDb.BuildDb() do! loadDocs () do! Custom.nonQuery - $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" [ SqliteParameter("@value", 100) ] - let! remaining = Count.all Db.tableName + let! remaining = Count.all SqliteDb.TableName Expect.equal remaining 5L "There should be 5 documents remaining in the table" } ] testTask "scalar succeeds" { - use! db = Db.buildDb () + 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" @@ -517,12 +508,12 @@ let all = ] testList "Extensions" [ testTask "ensureTable succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () let itExists (name: string) = task { let! result = conn.customScalar - $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it" + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" [ SqliteParameter("@name", name) ] _.GetInt64(0) return result > 0 @@ -541,23 +532,23 @@ let all = } testList "insert" [ testTask "succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll 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! conn.insert Db.tableName testDoc - let! after = conn.findAll Db.tableName + do! conn.insert SqliteDb.TableName testDoc + let! after = conn.findAll SqliteDb.TableName Expect.equal after [ testDoc ] "There should have been one document inserted" } testTask "fails for duplicate key" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - do! conn.insert Db.tableName { emptyDoc with Id = "test" } + do! conn.insert SqliteDb.TableName { emptyDoc with Id = "test" } Expect.throws (fun () -> - conn.insert Db.tableName {emptyDoc with Id = "test" } + conn.insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) "An exception should have been raised for duplicate document ID insert" @@ -565,96 +556,96 @@ let all = ] testList "save" [ testTask "succeeds when a document is inserted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll 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! conn.save Db.tableName testDoc - let! after = conn.findAll Db.tableName + do! conn.save SqliteDb.TableName testDoc + let! after = conn.findAll SqliteDb.TableName Expect.equal after [ testDoc ] "There should have been one document inserted" } testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! conn.insert Db.tableName testDoc + do! conn.insert SqliteDb.TableName testDoc - let! before = conn.findById Db.tableName "test" + let! before = conn.findById SqliteDb.TableName "test" if Option.isNone before then Expect.isTrue false "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! conn.save Db.tableName upd8Doc - let! after = conn.findById Db.tableName "test" + do! conn.save SqliteDb.TableName upd8Doc + let! after = conn.findById SqliteDb.TableName "test" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" Expect.equal after.Value upd8Doc "The updated document is not correct" } ] testTask "countAll succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! theCount = conn.countAll Db.tableName + let! theCount = conn.countAll SqliteDb.TableName Expect.equal theCount 5L "There should have been 5 matching documents" } testTask "countByField succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! theCount = conn.countByField Db.tableName "Value" EQ "purple" + let! theCount = conn.countByField SqliteDb.TableName "Value" EQ "purple" Expect.equal theCount 2L "There should have been 2 matching documents" } testList "existsById" [ testTask "succeeds when a document exists" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsById Db.tableName "three" + let! exists = conn.existsById SqliteDb.TableName "three" Expect.isTrue exists "There should have been an existing document" } testTask "succeeds when a document does not exist" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsById Db.tableName "seven" + let! exists = conn.existsById SqliteDb.TableName "seven" Expect.isFalse exists "There should not have been an existing document" } ] testList "existsByField" [ testTask "succeeds when documents exist" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField Db.tableName "NumValue" EQ 10 + let! exists = conn.existsByField SqliteDb.TableName "NumValue" EQ 10 Expect.isTrue exists "There should have been existing documents" } testTask "succeeds when no matching documents exist" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! exists = conn.existsByField Db.tableName "Nothing" EQ "none" + let! exists = conn.existsByField SqliteDb.TableName "Nothing" EQ "none" Expect.isFalse exists "There should not have been any existing documents" } ] testList "findAll" [ testTask "succeeds when there is data" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - do! insert Db.tableName { Foo = "one"; Bar = "two" } - do! insert Db.tableName { Foo = "three"; Bar = "four" } - do! insert Db.tableName { Foo = "five"; Bar = "six" } + 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 = conn.findAll Db.tableName + let! results = conn.findAll SqliteDb.TableName let expected = [ { Foo = "one"; Bar = "two" } { Foo = "three"; Bar = "four" } @@ -663,115 +654,115 @@ let all = Expect.equal results expected "There should have been 3 documents returned" } testTask "succeeds when there is no data" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! results = conn.findAll Db.tableName + let! results = conn.findAll SqliteDb.TableName Expect.equal results [] "There should have been no documents returned" } ] testList "findById" [ testTask "succeeds when a document is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findById Db.tableName "two" + let! doc = conn.findById 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findById Db.tableName "three hundred eighty-seven" + let! doc = conn.findById SqliteDb.TableName "three hundred eighty-seven" Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] testList "findByField" [ testTask "succeeds when documents are found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField Db.tableName "Sub.Foo" EQ "green" + let! docs = conn.findByField SqliteDb.TableName "Sub.Foo" EQ "green" Expect.equal (List.length docs) 2 "There should have been two documents returned" } testTask "succeeds when documents are not found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.findByField Db.tableName "Value" EQ "mauve" + let! docs = conn.findByField SqliteDb.TableName "Value" EQ "mauve" Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" } ] testList "findFirstByField" [ testTask "succeeds when a document is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField Db.tableName "Value" EQ "another" + let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField Db.tableName "Sub.Foo" EQ "green" + let! doc = conn.findFirstByField SqliteDb.TableName "Sub.Foo" EQ "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! doc = conn.findFirstByField Db.tableName "Value" EQ "absent" + let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "absent" Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] testList "updateFull" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! conn.updateFull Db.tableName "one" testDoc - let! after = conn.findById Db.tableName "one" + do! conn.updateFull SqliteDb.TableName "one" testDoc + let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test do! conn.updateFull - Db.tableName + SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] testList "updateFullFunc" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () do! conn.updateFullFunc - Db.tableName + SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = conn.findById Db.tableName "one" + let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" Expect.equal @@ -780,125 +771,125 @@ let all = "The updated document is not correct" } testTask "succeeds when no document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test do! conn.updateFullFunc - Db.tableName + SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] testList "updatePartialById" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.updatePartialById Db.tableName "one" {| NumValue = 44 |} - let! after = conn.findById Db.tableName "one" + do! conn.updatePartialById SqliteDb.TableName "one" {| NumValue = 44 |} + let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialById Db.tableName "test" {| Foo = "green" |} + do! conn.updatePartialById SqliteDb.TableName "test" {| Foo = "green" |} } ] testList "updatePartialByField" [ testTask "succeeds when a document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.updatePartialByField Db.tableName "Value" EQ "purple" {| NumValue = 77 |} - let! after = conn.countByField Db.tableName "NumValue" EQ 77 + do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = conn.countByField SqliteDb.TableName "NumValue" EQ 77 Expect.equal after 2L "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let! before = conn.findAll Db.tableName + let! before = conn.findAll SqliteDb.TableName Expect.hasCountOf before 0u isTrue "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialByField Db.tableName "Value" EQ "burgundy" {| Foo = "green" |} + do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] testList "deleteById" [ testTask "succeeds when a document is deleted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteById Db.tableName "four" - let! remaining = conn.countAll Db.tableName + do! conn.deleteById SqliteDb.TableName "four" + let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 4L "There should have been 4 documents remaining" } testTask "succeeds when a document is not deleted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteById Db.tableName "thirty" - let! remaining = conn.countAll Db.tableName + do! conn.deleteById SqliteDb.TableName "thirty" + let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } ] testList "deleteByField" [ testTask "succeeds when documents are deleted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField Db.tableName "Value" NE "purple" - let! remaining = conn.countAll Db.tableName + do! conn.deleteByField SqliteDb.TableName "Value" NE "purple" + let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 2L "There should have been 2 documents remaining" } testTask "succeeds when documents are not deleted" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.deleteByField Db.tableName "Value" EQ "crimson" - let! remaining = conn.countAll Db.tableName + do! conn.deleteByField SqliteDb.TableName "Value" EQ "crimson" + let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 5L "There should have been 5 documents remaining" } ] testList "customSingle" [ testTask "succeeds when a row is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () let! doc = conn.customSingle - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + $"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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () let! doc = conn.customSingle - $"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id" + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id" [ SqliteParameter("@id", "eighty") ] fromData Expect.isNone doc "There should not have been a document returned" @@ -906,21 +897,21 @@ let all = ] testList "customList" [ testTask "succeeds when data is found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - let! docs = conn.customList (Query.selectFromTable Db.tableName) [] fromData + let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" } testTask "succeeds when data is not found" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () let! docs = conn.customList - $"SELECT data FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" [ SqliteParameter("@value", 100) ] fromData Expect.isEmpty docs "There should have been no documents returned" @@ -928,30 +919,30 @@ let all = ] testList "customNonQuery" [ testTask "succeeds when operating on data" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.customNonQuery $"DELETE FROM {Db.tableName}" [] + do! conn.customNonQuery $"DELETE FROM {SqliteDb.TableName}" [] - let! remaining = conn.countAll Db.tableName + let! remaining = conn.countAll 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 = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () do! conn.customNonQuery - $"DELETE FROM {Db.tableName} WHERE data ->> 'NumValue' > @value" + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" [ SqliteParameter("@value", 100) ] - let! remaining = conn.countAll Db.tableName + let! remaining = conn.countAll SqliteDb.TableName Expect.equal remaining 5L "There should be 5 documents remaining in the table" } ] testTask "customScalar succeeds" { - use! db = Db.buildDb () + use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () let! nbr = conn.customScalar "SELECT 5 AS test_value" [] _.GetInt32(0) @@ -963,3 +954,5 @@ let all = } ] |> testSequenced + +let all = testList "Sqlite" [ unitTests; integrationTests ] -- 2.45.1 From a6d179d401089e45eb93ed3ae12a24da075b1163 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 25 Dec 2023 22:32:55 -0500 Subject: [PATCH 08/26] Switch to namespace from module for Sqlite - Complete C# test migration --- src/Sqlite/Library.fs | 41 +- src/Tests.CSharp/SqliteCSharpTests.cs | 1184 +++++++++++++------------ src/Tests.CSharp/SqliteDb.cs | 2 +- src/Tests/Program.fs | 2 +- 4 files changed, 658 insertions(+), 571 deletions(-) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 25dedcd..abee0aa 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -1,5 +1,4 @@ -/// Document store implementation for SQLite -module BitBadger.Documents.Sqlite +namespace BitBadger.Documents.Sqlite open BitBadger.Documents open Microsoft.Data.Sqlite @@ -106,11 +105,14 @@ module Results = ToCustomList<'TDoc>(cmd, fromData<'TDoc>) -/// Execute a non-query command -let internal write (cmd: SqliteCommand) = backgroundTask { - let! _ = cmd.ExecuteNonQueryAsync() - () -} +[] +module internal Helpers = + + /// Execute a non-query command + let internal write (cmd: SqliteCommand) = backgroundTask { + let! _ = cmd.ExecuteNonQueryAsync() + () + } /// Versions of queries that accept a SqliteConnection as the last parameter @@ -373,17 +375,21 @@ module Definition = use conn = Configuration.dbConn () WithConn.Definition.ensureTable name conn -/// Insert a new document -[] -let insert<'TDoc> tableName (document: 'TDoc) = - use conn = Configuration.dbConn () - WithConn.insert tableName document conn +/// Document insert/save functions +[] +module Document = + + /// Insert a new document + [] + let insert<'TDoc> tableName (document: 'TDoc) = + use conn = Configuration.dbConn () + WithConn.insert tableName document conn -/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") -[] -let save<'TDoc> tableName (document: 'TDoc) = - use conn = Configuration.dbConn () - WithConn.save 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 /// Commands to count documents [] @@ -613,6 +619,7 @@ module Extensions = open System.Runtime.CompilerServices /// C# extensions on the SqliteConnection type +[] type SqliteConnectionCSharpExtensions = /// Execute a query that returns a list of results diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index c0f9873..73baea0 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -2,17 +2,51 @@ using Expecto; using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; -using Docs = BitBadger.Documents; +using BitBadger.Documents.Sqlite; namespace BitBadger.Documents.Tests.CSharp; -using static Sqlite; +using static Runner; /// /// C# tests for the SQLite implementation of BitBadger.Documents /// public static class SqliteCSharpTests { + /// + /// Unit tests for the SQLite library + /// + public static Test Unit = + TestList("Unit", new[] + { + TestList("Parameters", new[] + { + 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("Field succeeds", () => + { + var theParam = Parameters.Field(99); + Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); + Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); + }), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); + }) + }) + // Results are exhaustively executed in the context of other tests + }); + private static readonly List TestDocuments = new() { new() { Id = "one", Value = "FIRST!", NumValue = 0 }, @@ -24,32 +58,32 @@ public static class SqliteCSharpTests private static async Task LoadDocs() { - foreach (var doc in TestDocuments) await Insert(SqliteDb.TableName, doc); + foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } [Tests] public static Test Integration = - Runner.TestList("Sqlite.C# Integration", new[] + TestList("Integration", new[] { - Runner.TestCase("Configuration.UseConnectionString succeeds", () => + TestCase("Configuration.UseConnectionString succeeds", () => { try { - Configuration.UseConnectionString("Data Source=test.db"); - Expect.equal(Configuration.connectionString, + 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 { - Configuration.UseConnectionString("Data Source=:memory:"); + Sqlite.Configuration.UseConnectionString("Data Source=:memory:"); } }), - Runner.TestList("Custom", new[] + TestList("Custom", new[] { - Runner.TestList("Single", new [] + TestList("Single", new [] { - Runner.TestCase("succeeds when a row is found", async () => + TestCase("succeeds when a row is found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -57,10 +91,10 @@ public static class SqliteCSharpTests var doc = await Custom.Single( $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'Id' = @id", new[] { Parameters.Id("one") }, Results.FromData); - if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "one", "The incorrect document was returned"); }), - Runner.TestCase("succeeds when a row is not found", async () => + TestCase("succeeds when a row is not found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -71,18 +105,18 @@ public static class SqliteCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) }), - Runner.TestList("List", new[] + TestList("List", new[] { - Runner.TestCase("succeeds when data is found", async () => + TestCase("succeeds when data is found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var docs = await Custom.List(Docs.Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + 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"); }), - Runner.TestCase("succeeds when data is not found", async () => + TestCase("succeeds when data is not found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -93,9 +127,9 @@ public static class SqliteCSharpTests Expect.isEmpty(docs, "There should have been no documents returned"); }) }), - Runner.TestList("NonQuery", new[] + TestList("NonQuery", new[] { - Runner.TestCase("succeeds when operating on data", async () => + TestCase("succeeds when operating on data", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -105,7 +139,7 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); }), - Runner.TestCase("succeeds when no data matches where clause", async () => + TestCase("succeeds when no data matches where clause", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -118,7 +152,7 @@ public static class SqliteCSharpTests Expect.equal(remaining, 5L, "There should be 5 documents remaining in the table"); }) }), - Runner.TestCase("Scalar succeeds", async () => + TestCase("Scalar succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -126,9 +160,9 @@ public static class SqliteCSharpTests Expect.equal(nbr, 5, "The query should have returned the number 5"); }) }), - Runner.TestList("Definition", new[] + TestList("Definition", new[] { - Runner.TestCase("EnsureTable succeeds", async () => + TestCase("EnsureTable succeeds", async () => { await using var db = await SqliteDb.BuildDb(); @@ -155,25 +189,25 @@ public static class SqliteCSharpTests } }) }), - Runner.TestList("Insert", new[] + TestList("Document.Insert", new[] { - Runner.TestCase("succeeds", async () => + TestCase("succeeds", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should be no documents in the table"); - await Insert(SqliteDb.TableName, + Expect.isEmpty(before, "There should be no documents in the table"); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); var after = await Find.All(SqliteDb.TableName); Expect.equal(after.Count, 1, "There should have been one document inserted"); }), - Runner.TestCase("fails for duplicate key", async () => + TestCase("fails for duplicate key", async () => { await using var db = await SqliteDb.BuildDb(); - await Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); try { - await Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); } catch (Exception) @@ -182,42 +216,42 @@ public static class SqliteCSharpTests } }) }), - Runner.TestList("Save", new[] + TestList("Document.Save", new[] { - Runner.TestCase("succeeds when a document is inserted", async () => + TestCase("succeeds when a document is inserted", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should be no documents in the table"); + Expect.isEmpty(before, "There should be no documents in the table"); - await Save(SqliteDb.TableName, + await Document.Save(SqliteDb.TableName, new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); var after = await Find.All(SqliteDb.TableName); Expect.equal(after.Count, 1, "There should have been one document inserted"); }), - Runner.TestCase("succeeds when a document is updated", async () => + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); - await Insert(SqliteDb.TableName, + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); var before = await Find.ById(SqliteDb.TableName, "test"); - if (before is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.isNotNull(before, "There should have been a document returned"); Expect.equal(before!.Id, "test", "The document is not correct"); Expect.isNotNull(before.Sub, "There should have been a sub-document"); Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); Expect.equal(before.Sub.Bar, "b", "The document is not correct"); - await Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + await Document.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); var after = await Find.ById(SqliteDb.TableName, "test"); - if (after is null) Expect.isTrue(false, "There should have been a document returned post-update"); + Expect.isNotNull(after, "There should have been a document returned post-update"); 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"); }) }), - Runner.TestList("Count", new[] + TestList("Count", new[] { - Runner.TestCase("All succeeds", async () => + TestCase("All succeeds", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -225,7 +259,7 @@ public static class SqliteCSharpTests var theCount = await Count.All(SqliteDb.TableName); Expect.equal(theCount, 5L, "There should have been 5 matching documents"); }), - Runner.TestCase("ByField succeeds", async () => + TestCase("ByField succeeds", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -234,11 +268,11 @@ public static class SqliteCSharpTests Expect.equal(theCount, 2L, "There should have been 2 matching documents"); }) }), - Runner.TestList("Exists", new[] + TestList("Exists", new[] { - Runner.TestList("ById", new[] + TestList("ById", new[] { - Runner.TestCase("succeeds when a document exists", async () => + TestCase("succeeds when a document exists", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -246,7 +280,7 @@ public static class SqliteCSharpTests var exists = await Exists.ById(SqliteDb.TableName, "three"); Expect.isTrue(exists, "There should have been an existing document"); }), - Runner.TestCase("succeeds when a document does not exist", async () => + TestCase("succeeds when a document does not exist", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -255,9 +289,9 @@ public static class SqliteCSharpTests Expect.isFalse(exists, "There should not have been an existing document"); }) }), - Runner.TestList("ByField", new[] + TestList("ByField", new[] { - Runner.TestCase("succeeds when documents exist", async () => + TestCase("succeeds when documents exist", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -265,7 +299,7 @@ public static class SqliteCSharpTests var exists = await Exists.ByField(SqliteDb.TableName, "NumValue", Op.GE, 10); Expect.isTrue(exists, "There should have been existing documents"); }), - Runner.TestCase("succeeds when no matching documents exist", async () => + TestCase("succeeds when no matching documents exist", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -275,40 +309,40 @@ public static class SqliteCSharpTests }) }) }), - Runner.TestList("Find", new[] + TestList("Find", new[] { - Runner.TestList("All", new[] + TestList("All", new[] { - Runner.TestCase("succeeds when there is data", async () => + TestCase("succeeds when there is data", async () => { await using var db = await SqliteDb.BuildDb(); - await Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); - await Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); - await Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + 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"); }), - Runner.TestCase("succeeds when there is no data", async () => + TestCase("succeeds when there is no data", async () => { await using var db = await SqliteDb.BuildDb(); var results = await Find.All(SqliteDb.TableName); - Expect.equal(results.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(results, "There should have been no documents returned"); }) }), - Runner.TestList("ById", new[] + TestList("ById", new[] { - Runner.TestCase("succeeds when a document is found", async () => + 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"); - if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned"); }), - Runner.TestCase("succeeds when a document is not found", async () => + TestCase("succeeds when a document is not found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -317,9 +351,9 @@ public static class SqliteCSharpTests Expect.isNull(doc, "There should not have been a document returned"); }) }), - Runner.TestList("ByField", new[] + TestList("ByField", new[] { - Runner.TestCase("succeeds when documents are found", async () => + TestCase("succeeds when documents are found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -327,36 +361,36 @@ public static class SqliteCSharpTests var docs = await Find.ByField(SqliteDb.TableName, "NumValue", Op.GT, 15); Expect.equal(docs.Count, 2, "There should have been two documents returned"); }), - Runner.TestCase("succeeds when documents are not found", async () => + TestCase("succeeds when documents are not found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); var docs = await Find.ByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); - Expect.equal(docs.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(docs, "There should have been no documents returned"); }) }), - Runner.TestList("FirstByField", new[] + TestList("FirstByField", new[] { - Runner.TestCase("succeeds when a document is found", async () => + TestCase("succeeds when a document is found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); var doc = await Find.FirstByField(SqliteDb.TableName, "Value", Op.EQ, "another"); - if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.isNotNull(doc, "There should have been a document returned"); Expect.equal(doc!.Id, "two", "The incorrect document was returned"); }), - Runner.TestCase("succeeds when multiple documents are found", async () => + TestCase("succeeds when multiple documents are found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); var doc = await Find.FirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, "green"); - if (doc is null) Expect.isTrue(false, "There should have been a document returned"); + Expect.isNotNull(doc, "There should have been a document returned"); Expect.contains(new[] { "two", "four" }, doc!.Id, "An incorrect document was returned"); }), - Runner.TestCase("succeeds when a document is not found", async () => + TestCase("succeeds when a document is not found", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -366,11 +400,11 @@ public static class SqliteCSharpTests }) }) }), - Runner.TestList("Update", new[] + TestList("Update", new[] { - Runner.TestList("Full", new[] + TestList("Full", new[] { - Runner.TestCase("succeeds when a document is updated", async () => + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -378,28 +412,27 @@ public static class SqliteCSharpTests var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; await Update.Full(SqliteDb.TableName, "one", testDoc); var after = await Find.ById(SqliteDb.TableName, "one"); - if (after is null) - Expect.isTrue(false, "There should have been a document returned post-update"); + 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"); }), - Runner.TestCase("succeeds when no document is updated", async () => + TestCase("succeeds when no document is updated", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test await Update.Full(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - Runner.TestList("FullFunc", new[] + TestList("FullFunc", new[] { - Runner.TestCase("succeeds when a document is updated", async () => + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -407,53 +440,51 @@ public static class SqliteCSharpTests await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); var after = await Find.ById(SqliteDb.TableName, "one"); - if (after is null) - Expect.isTrue(false, "There should have been a document returned post-update"); + 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"); }), - Runner.TestCase("succeeds when no document is updated", async () => + TestCase("succeeds when no document is updated", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) }), - Runner.TestList("PartialById", new[] + TestList("PartialById", new[] { - Runner.TestCase("succeeds when a document is updated", async () => + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); await Update.PartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); var after = await Find.ById(SqliteDb.TableName, "one"); - if (after is null) - Expect.isTrue(false, "There should have been a document returned post-update"); + 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"); }), - Runner.TestCase("succeeds when no document is updated", async () => + TestCase("succeeds when no document is updated", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test await Update.PartialById(SqliteDb.TableName, "test", new { Foo = "green" }); }) }), - Runner.TestList("PartialByField", new[] + TestList("PartialByField", new[] { - Runner.TestCase("succeeds when a document is updated", async () => + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -463,12 +494,12 @@ public static class SqliteCSharpTests var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 77); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), - Runner.TestCase("succeeds when no document is updated", async () => + TestCase("succeeds when no document is updated", async () => { await using var db = await SqliteDb.BuildDb(); var before = await Find.All(SqliteDb.TableName); - Expect.equal(before.Count, 0, "There should have been no documents returned"); + Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", @@ -476,11 +507,11 @@ public static class SqliteCSharpTests }) }) }), - Runner.TestList("Delete", new[] + TestList("Delete", new[] { - Runner.TestList("ById", new[] + TestList("ById", new[] { - Runner.TestCase("succeeds when a document is deleted", async () => + TestCase("succeeds when a document is deleted", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -489,7 +520,7 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); }), - Runner.TestCase("succeeds when a document is not deleted", async () => + TestCase("succeeds when a document is not deleted", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -499,9 +530,9 @@ public static class SqliteCSharpTests Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) }), - Runner.TestList("ByField", new[] + TestList("ByField", new[] { - Runner.TestCase("succeeds when documents are deleted", async () => + TestCase("succeeds when documents are deleted", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -510,7 +541,7 @@ public static class SqliteCSharpTests var remaining = await Count.All(SqliteDb.TableName); Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); }), - Runner.TestCase("succeeds when documents are not deleted", async () => + TestCase("succeeds when documents are not deleted", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); @@ -521,456 +552,505 @@ public static class SqliteCSharpTests }) }) }), - // Runner.TestList("Extensions" [ - // Runner.TestCase("EnsureTable succeeds" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let itExists (name: string) = task { - // let! result = - // conn.CustomScalar( - // $"SELECT EXISTS (SELECT 1 FROM {Db.catalog} WHERE name = @name) AS it", - // [ SqliteParameter("@name", name) ], - // System.Func _.GetInt64(0)) - // return result > 0L - // } - // - // 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" - // - // await 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" - // } - // Runner.TestList("Insert" [ - // Runner.TestCase("succeeds" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" - // await conn.Insert( - // SqliteDb.TableName , - // JsonDocument(Id = "turkey", Sub = Some (SubDocument(Foo = "gobble", Bar = "gobble")))) - // let! after = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" - // } - // Runner.TestCase("fails for duplicate key" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "test")) - // Expect.throws - // (fun () -> - // conn.Insert(SqliteDb.TableName , JsonDocument(Id = "test")) - // |> Async.AwaitTask - // |> Async.RunSynchronously) - // "An exception should have been raised for duplicate document ID insert" - // } - // ] - // Runner.TestList("Save" [ - // Runner.TestCase("succeeds when a document is inserted" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should be no documents in the table" - // - // await conn.Save( - // SqliteDb.TableName , - // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) - // let! after = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf after 1u isTrue "There should have been one document inserted" - // } - // Runner.TestCase("succeeds when a document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await conn.Insert( - // SqliteDb.TableName , - // JsonDocument(Id = "test", Sub = Some (SubDocument(Foo = "a", Bar = "b")))) - // - // let! before = conn.FindById(SqliteDb.TableName , "test") - // if isNull before then Expect.isTrue false "There should have been a document returned" - // let before = before :> JsonDocument - // Expect.equal before.Id "test" "The document is not correct" - // Expect.isSome before.Sub "There should have been a sub-document" - // Expect.equal before.Sub.Value.Foo "a" "The document is not correct" - // Expect.equal before.Sub.Value.Bar "b" "The document is not correct" - // - // await Save(SqliteDb.TableName , JsonDocument(Id = "test")) - // let! after = conn.FindById(SqliteDb.TableName , "test") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // Expect.equal after.Id "test" "The updated document is not correct" - // Expect.isNone after.Sub "There should not have been a sub-document in the updated document" - // } - // ] - // Runner.TestCase("CountAll succeeds" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! theCount = conn.CountAll SqliteDb.TableName - // Expect.equal theCount 5L "There should have been 5 matching documents" - // } - // Runner.TestCase("CountByField succeeds" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! theCount = conn.CountByField(SqliteDb.TableName , "Value", Op.EQ, "purple") - // Expect.equal theCount 2L "There should have been 2 matching documents" - // } - // Runner.TestList("ExistsById" [ - // Runner.TestCase("succeeds when a document exists" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! exists = conn.ExistsById(SqliteDb.TableName , "three") - // Expect.isTrue exists "There should have been an existing document" - // } - // Runner.TestCase("succeeds when a document does not exist" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! exists = conn.ExistsById(SqliteDb.TableName , "seven") - // Expect.isFalse exists "There should not have been an existing document" - // } - // ] - // Runner.TestList("ExistsByField" [ - // Runner.TestCase("succeeds when documents exist" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! exists = conn.ExistsByField(SqliteDb.TableName , "NumValue", Op.GE, 10) - // Expect.isTrue exists "There should have been existing documents" - // } - // Runner.TestCase("succeeds when no matching documents exist" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! exists = conn.ExistsByField(SqliteDb.TableName , "Nothing", Op.EQ, "none") - // Expect.isFalse exists "There should not have been any existing documents" - // } - // ] - // Runner.TestList("FindAll" [ - // Runner.TestCase("succeeds when there is data" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // - // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "one", Value = "two")) - // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "three", Value = "four")) - // await conn.Insert(SqliteDb.TableName , JsonDocument(Id = "five", Value = "six")) - // - // let! results = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf results 3u isTrue "There should have been 3 documents returned" - // } - // Runner.TestCase("succeeds when there is no data" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! results = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf results 0u isTrue "There should have been no documents returned" - // } - // ] - // Runner.TestList("FindById" [ - // Runner.TestCase("succeeds when a document is found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = conn.FindById(SqliteDb.TableName , "two") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" - // } - // Runner.TestCase("succeeds when a document is not found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = conn.FindById(SqliteDb.TableName , "three hundred eighty-seven") - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // Runner.TestList("FindByField" [ - // Runner.TestCase("succeeds when documents are found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! docs = conn.FindByField(SqliteDb.TableName , "NumValue", Op.GT, 15) - // Expect.hasCountOf docs 2u isTrue "There should have been two documents returned" - // } - // Runner.TestCase("succeeds when documents are not found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! docs = conn.FindByField(SqliteDb.TableName , "Value", Op.EQ, "mauve") - // Expect.hasCountOf docs 0u isTrue "There should have been no documents returned" - // } - // ] - // Runner.TestList("FindFirstByField" [ - // Runner.TestCase("succeeds when a document is found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Value", Op.EQ, "another") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "two" "The incorrect document was returned" - // } - // Runner.TestCase("succeeds when multiple documents are found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Sub.Foo", Op.EQ, "green") - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.contains [ "two"; "four" ] (doc :> JsonDocument).Id "An incorrect document was returned" - // } - // Runner.TestCase("succeeds when a document is not found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = conn.FindFirstByField(SqliteDb.TableName , "Value", Op.EQ, "absent") - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // Runner.TestList("UpdateFull" [ - // Runner.TestCase("succeeds when a document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let testDoc = JsonDocument(Id = "one", Sub = Some (SubDocument(Foo = "blue", Bar = "red"))) - // await conn.UpdateFull(SqliteDb.TableName , "one", testDoc) - // let! after = conn.FindById(SqliteDb.TableName , "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // Expect.equal after.Id "one" "The updated document is not correct" - // Expect.isSome after.Sub "The updated document should have had a sub-document" - // Expect.equal after.Sub.Value.Foo "blue" "The updated sub-document is not correct" - // Expect.equal after.Sub.Value.Bar "red" "The updated sub-document is not correct" - // } - // Runner.TestCase("succeeds when no document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // await conn.UpdateFull( - // SqliteDb.TableName , - // "test", - // JsonDocument(Id = "x", Sub = Some (SubDocument(Foo = "blue", Bar = "red")))) - // } - // ] - // Runner.TestList("UpdateFullFunc" [ - // Runner.TestCase("succeeds when a document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.UpdateFullFunc( - // SqliteDb.TableName , - // System.Func _.Id, - // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) - // let! after = conn.FindById(SqliteDb.TableName , "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // 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.isNone after.Sub "The updated document should not have a sub-document" - // } - // Runner.TestCase("succeeds when no document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // await conn.UpdateFullFunc( - // SqliteDb.TableName , - // System.Func _.Id, - // JsonDocument(Id = "one", Value = "le un", NumValue = 1, Sub = None)) - // } - // ] - // Runner.TestList("UpdatePartialById" [ - // Runner.TestCase("succeeds when a document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.UpdatePartialById(SqliteDb.TableName , "one", {| NumValue = 44 |}) - // let! after = conn.FindById(SqliteDb.TableName , "one") - // if isNull after then Expect.isTrue false "There should have been a document returned post-update" - // let after = after :> JsonDocument - // Expect.equal after.Id "one" "The updated document is not correct" - // Expect.equal after.NumValue 44 "The updated document is not correct" - // } - // Runner.TestCase("succeeds when no document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // await conn.UpdatePartialById(SqliteDb.TableName , "test", {| Foo = "green" |}) - // } - // ] - // Runner.TestList("UpdatePartialByField" [ - // Runner.TestCase("succeeds when a document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.UpdatePartialByField(SqliteDb.TableName , "Value", Op.EQ, "purple", {| NumValue = 77 |}) - // let! after = conn.CountByField(SqliteDb.TableName , "NumValue", Op.EQ, 77) - // Expect.equal after 2L "There should have been 2 documents returned" - // } - // Runner.TestCase("succeeds when no document is updated" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // let! before = conn.FindAll SqliteDb.TableName - // Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - // - // // This not raising an exception is the test - // await conn.UpdatePartialByField(SqliteDb.TableName , "Value", Op.EQ, "burgundy", {| Foo = "green" |}) - // } - // ] - // Runner.TestList("DeleteById" [ - // Runner.TestCase("succeeds when a document is deleted" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.DeleteById(SqliteDb.TableName , "four") - // let! remaining = conn.CountAll SqliteDb.TableName - // Expect.equal remaining 4L "There should have been 4 documents remaining" - // } - // Runner.TestCase("succeeds when a document is not deleted" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.DeleteById(SqliteDb.TableName , "thirty") - // let! remaining = conn.CountAll SqliteDb.TableName - // Expect.equal remaining 5L "There should have been 5 documents remaining" - // } - // ] - // Runner.TestList("DeleteByField" [ - // Runner.TestCase("succeeds when documents are deleted" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.DeleteByField(SqliteDb.TableName , "Value", Op.NE, "purple") - // let! remaining = conn.CountAll SqliteDb.TableName - // Expect.equal remaining 2L "There should have been 2 documents remaining" - // } - // Runner.TestCase("succeeds when documents are not deleted" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.DeleteByField(SqliteDb.TableName , "Value", Op.EQ, "crimson") - // let! remaining = Count.All SqliteDb.TableName - // Expect.equal remaining 5L "There should have been 5 documents remaining" - // } - // ] - // Runner.TestList("CustomSingle" [ - // Runner.TestCase("succeeds when a row is found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = - // conn.CustomSingle( - // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'Id' = @id", - // [ SqliteParameter("@id", "one") ], - // FromData) - // if isNull doc then Expect.isTrue false "There should have been a document returned" - // Expect.equal (doc :> JsonDocument).Id "one" "The incorrect document was returned" - // } - // Runner.TestCase("succeeds when a row is not found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! doc = - // conn.CustomSingle( - // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'Id' = @id", - // [ SqliteParameter("@id", "eighty") ], - // FromData) - // Expect.isNull doc "There should not have been a document returned" - // } - // ] - // Runner.TestList("CustomList" [ - // Runner.TestCase("succeeds when data is found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! docs = conn.CustomList(Query.SelectFromTable SqliteDb.TableName , [], FromData) - // Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" - // } - // Runner.TestCase("succeeds when data is not found" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // let! docs = - // conn.CustomList( - // $"SELECT data FROM {SqliteDb.TableName } WHERE data ->> 'NumValue' > @value", - // [ SqliteParameter("@value", 100) ], - // FromData) - // Expect.isEmpty docs "There should have been no documents returned" - // } - // ] - // Runner.TestList("CustomNonQuery" [ - // Runner.TestCase("succeeds when operating on data" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName }", []) - // - // let! remaining = conn.CountAll SqliteDb.TableName - // Expect.equal remaining 0L "There should be no documents remaining in the table" - // } - // Runner.TestCase("succeeds when no data matches where clause" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // await LoadDocs(); - // - // await conn.CustomNonQuery( - // $"DELETE FROM {SqliteDb.TableName } WHERE data ->> 'NumValue' > @value", - // [ SqliteParameter("@value", 100) ]) - // - // let! remaining = conn.CountAll SqliteDb.TableName - // Expect.equal remaining 5L "There should be 5 documents remaining in the table" - // } - // ] - // Runner.TestCase("CustomScalar succeeds" { - // use! db = Db.buildDb () - // use conn = Configuration.DbConn() - // - // let! nbr = - // conn.CustomScalar("SELECT 5 AS test_value", [], System.Func _.GetInt32(0)) - // Expect.equal nbr 5 "The query should have returned the number 5" - // } - // ] - // test "clean up database" { - // Configuration.UseConnectionString "data source=:memory:" - // } - // } - // ] - // |> testSequenced) + TestList("Extensions", new[] + { + TestList("CustomSingle", new[] + { + TestCase("succeeds when a row is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.CustomSingle( + $"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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.CustomSingle( + $"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("CustomList", new[] + { + 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, + 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.CustomList( + $"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("CustomNonQuery", new[] + { + TestCase("succeeds when operating on data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); + + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.CustomNonQuery( + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", + new[] { new SqliteParameter("@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(); + await using var conn = Sqlite.Configuration.DbConn(); + + var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, + rdr => rdr.GetInt32(0)); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }), + TestCase("EnsureTable succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + Func> itExists = async name => + { + var result = await conn.CustomScalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + new SqliteParameter[] { new("@name", name) }, + rdr => rdr.GetInt64(0)); + return result > 0L; + }; + + 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"); + Expect.isTrue(exists, "The table should now exist"); + Expect.isTrue(alsoExists, "The key index should now exist"); + }), + TestList("Insert", new[] + { + TestCase("succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + try + { + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, + "An exception should have been raised for duplicate document ID insert"); + } + catch (Exception) + { + // This is what is supposed to happen + } + }) + }), + TestList("Save", new[] + { + TestCase("succeeds when a document is inserted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + + await conn.Save(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before!.Id, "test", "The document is not correct"); + Expect.isNotNull(before.Sub, "There should have been a sub-document"); + Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); + Expect.equal(before.Sub.Bar, "b", "The document is not correct"); + + await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + var after = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + 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(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountAll(SqliteDb.TableName); + Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + }), + TestCase("CountByField succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + }), + TestList("ExistsById", new[] + { + TestCase("succeeds when a document exists", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsById(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + }), + TestList("ExistsByField", new[] + { + 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, "NumValue", Op.GE, 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }), + TestList("FindAll", new[] + { + TestCase("succeeds when there is data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + + var results = await conn.FindAll(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var results = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + }), + TestList("FindById", new[] + { + 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.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("FindByField", new[] + { + 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, "NumValue", Op.GT, 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindFirstByField", new[] + { + 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, "Value", Op.EQ, + "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, + "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Value", Op.EQ, + "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("UpdateFull", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); + var after = await conn.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFull(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + TestList("UpdateFullFunc", new[] + { + 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.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("UpdatePartialById", new[] + { + 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.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByField", new[] + { + 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.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", + new { NumValue = 77 }); + var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); + }) + }), + TestList("DeleteById", new[] + { + TestCase("succeeds when a document is deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "four"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "thirty"); + var remaining = await conn.CountAll(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByField", new[] + { + 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, "Value", Op.NE, "purple"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "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:")) }); + + /// + /// All tests for SQLite C# functions and methods + /// + public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) }); } diff --git a/src/Tests.CSharp/SqliteDb.cs b/src/Tests.CSharp/SqliteDb.cs index 876ab46..cf7fe5e 100644 --- a/src/Tests.CSharp/SqliteDb.cs +++ b/src/Tests.CSharp/SqliteDb.cs @@ -3,7 +3,7 @@ namespace BitBadger.Documents.Tests; using System; using System.IO; using System.Threading.Tasks; -using static Sqlite; +using Sqlite; /// /// A throwaway SQLite database file, which will be deleted when it goes out of scope diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 80749e8..9480cb0 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -7,7 +7,7 @@ let allTests = [ CommonTests.all CommonCSharpTests.Unit SqliteTests.all - testSequenced SqliteCSharpTests.Integration ] + SqliteCSharpTests.All ] [] let main args = runTestsWithCLIArgs [] args allTests -- 2.45.1 From f2bb1c4abacbec91cc683d643762b4609e277a76 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 25 Dec 2023 23:02:29 -0500 Subject: [PATCH 09/26] Move extensions to separate project - Move tests into separate files --- src/BitBadger.Documents.sln | 6 + ...tBadger.Documents.Sqlite.Extensions.fsproj | 11 + src/Sqlite.Extensions/Library.fs | 216 ++++++ src/Sqlite/Library.fs | 214 ------ .../BitBadger.Documents.Tests.CSharp.csproj | 1 + .../SqliteCSharpExtensionTests.cs | 511 ++++++++++++++ src/Tests.CSharp/SqliteCSharpTests.cs | 496 +------------- src/Tests/BitBadger.Documents.Tests.fsproj | 3 + src/Tests/Program.fs | 4 +- src/Tests/SqliteExtensionTests.fs | 467 +++++++++++++ src/Tests/SqliteTests.fs | 631 +++--------------- src/Tests/Types.fs | 13 + 12 files changed, 1317 insertions(+), 1256 deletions(-) create mode 100644 src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj create mode 100644 src/Sqlite.Extensions/Library.fs create mode 100644 src/Tests.CSharp/SqliteCSharpExtensionTests.cs create mode 100644 src/Tests/SqliteExtensionTests.fs create mode 100644 src/Tests/Types.fs diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln index f3d49d0..00d5639 100644 --- a/src/BitBadger.Documents.sln +++ b/src/BitBadger.Documents.sln @@ -13,6 +13,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.CSharp", "Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj", "{AB58418C-7F90-467E-8F67-F4E0AD9D8875}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite.Extensions", "Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj", "{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.Build.0 = Release|Any CPU + {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj b/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj new file mode 100644 index 0000000..e01f9dc --- /dev/null +++ b/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Sqlite.Extensions/Library.fs b/src/Sqlite.Extensions/Library.fs new file mode 100644 index 0000000..011746e --- /dev/null +++ b/src/Sqlite.Extensions/Library.fs @@ -0,0 +1,216 @@ +namespace BitBadger.Documents.Sqlite + +open Microsoft.Data.Sqlite + +/// F# extensions for the SqliteConnection type +[] +module Extensions = + + type SqliteConnection with + + /// Execute a query that returns a list of results + member conn.customList<'TDoc> query parameters mapFunc = + WithConn.Custom.list<'TDoc> query parameters mapFunc conn + + /// Execute a query that returns one or no results + member conn.customSingle<'TDoc> query parameters mapFunc = + WithConn.Custom.single<'TDoc> query parameters mapFunc conn + + /// Execute a query that does not return a value + member conn.customNonQuery query parameters = + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + member conn.customScalar<'T when 'T: struct> query parameters mapFunc = + WithConn.Custom.scalar<'T> query parameters mapFunc conn + + /// Create a document table + member conn.ensureTable name = + WithConn.Definition.ensureTable name conn + + /// Create an index on a document table + member conn.ensureIndex tableName indexName fields = + WithConn.Definition.ensureIndex tableName indexName fields conn + + /// Insert a new document + member conn.insert<'TDoc> tableName (document: 'TDoc) = + WithConn.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 + + /// 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 fieldName op (value: obj) = + WithConn.Count.byField tableName fieldName op value 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 fieldName op (value: obj) = + WithConn.Exists.byField tableName fieldName op value conn + + /// Retrieve all documents in the given table + member conn.findAll<'TDoc> tableName = + WithConn.Find.all<'TDoc> tableName 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 fieldName op (value: obj) = + WithConn.Find.byField<'TDoc> tableName fieldName op value conn + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) = + WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn + + /// Update an entire document + member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = + WithConn.Update.full tableName docId document conn + + /// Update an entire document + member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithConn.Update.fullFunc tableName idFunc document conn + + /// Update a partial document + member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPatch) = + WithConn.Update.partialById tableName docId partial conn + + /// Update partial documents using a comparison on a JSON field + member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPatch) = + WithConn.Update.partialByField tableName fieldName op value partial 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 fieldName op (value: obj) = + WithConn.Delete.byField tableName fieldName op value conn + + +open System.Runtime.CompilerServices + +/// C# extensions on the SqliteConnection type +[] +type SqliteConnectionCSharpExtensions = + + /// Execute a query that returns a list of results + [] + static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that returns one or no results + [] + static member inline CustomSingle<'TDoc when 'TDoc: null>( + conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) + + /// Execute a query that does not return a value + [] + static member inline CustomNonQuery(conn, query, parameters) = + WithConn.Custom.nonQuery query parameters conn + + /// Execute a query that returns a scalar value + [] + static member inline CustomScalar<'T when 'T: struct>( + conn, query, parameters, mapFunc: System.Func) = + WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) + + /// Create a document table + [] + static member inline EnsureTable(conn, name) = + WithConn.Definition.ensureTable name conn + + /// Create an index on one or more fields in a document table + [] + static member inline EnsureIndex(conn, tableName, indexName, fields) = + WithConn.Definition.ensureIndex tableName indexName fields conn + + /// Insert a new document + [] + static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = + WithConn.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 + + /// 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 + [] + static member inline CountByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Count.byField tableName fieldName op value 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 + [] + static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Exists.byField tableName fieldName op value conn + + /// Retrieve all documents in the given table + [] + static member inline FindAll<'TDoc>(conn, tableName) = + WithConn.Find.All<'TDoc>(tableName, 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 + [] + static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value) = + WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn) + + /// Retrieve documents via a comparison on a JSON field, returning only the first result + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) = + WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn) + + /// Update an entire document + [] + static member inline UpdateFull<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = + WithConn.Update.full tableName docId document conn + + /// Update an entire document + [] + static member inline UpdateFullFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) = + WithConn.Update.FullFunc(tableName, idFunc, doc, conn) + + /// Update a partial document + [] + static member inline UpdatePartialById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, partial: 'TPatch) = + WithConn.Update.partialById tableName docId partial conn + + /// Update partial documents using a comparison on a JSON field + [] + static member inline UpdatePartialByField<'TPatch>(conn, tableName, fieldName, op, value: obj, partial: 'TPatch) = + WithConn.Update.partialByField tableName fieldName op value partial 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 a JSON field + [] + static member inline DeleteByField(conn, tableName, fieldName, op, value: obj) = + WithConn.Delete.byField tableName fieldName op value conn diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index abee0aa..7f372db 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -519,217 +519,3 @@ module Delete = let byField tableName fieldName op (value: obj) = use conn = Configuration.dbConn () WithConn.Delete.byField tableName fieldName op value conn - - -/// F# extensions for the SqliteConnection type -[] -module Extensions = - - type SqliteConnection with - - /// Execute a query that returns a list of results - member conn.customList<'TDoc> query parameters mapFunc = - WithConn.Custom.list<'TDoc> query parameters mapFunc conn - - /// Execute a query that returns one or no results - member conn.customSingle<'TDoc> query parameters mapFunc = - WithConn.Custom.single<'TDoc> query parameters mapFunc conn - - /// Execute a query that does not return a value - member conn.customNonQuery query parameters = - WithConn.Custom.nonQuery query parameters conn - - /// Execute a query that returns a scalar value - member conn.customScalar<'T when 'T: struct> query parameters mapFunc = - WithConn.Custom.scalar<'T> query parameters mapFunc conn - - /// Create a document table - member conn.ensureTable name = - WithConn.Definition.ensureTable name conn - - /// Create an index on a document table - member conn.ensureIndex tableName indexName fields = - WithConn.Definition.ensureIndex tableName indexName fields conn - - /// Insert a new document - member conn.insert<'TDoc> tableName (document: 'TDoc) = - WithConn.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 - - /// 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 fieldName op (value: obj) = - WithConn.Count.byField tableName fieldName op value 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 fieldName op (value: obj) = - WithConn.Exists.byField tableName fieldName op value conn - - /// Retrieve all documents in the given table - member conn.findAll<'TDoc> tableName = - WithConn.Find.all<'TDoc> tableName 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 fieldName op (value: obj) = - WithConn.Find.byField<'TDoc> tableName fieldName op value conn - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) = - WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn - - /// Update an entire document - member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = - WithConn.Update.full tableName docId document conn - - /// Update an entire document - member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = - WithConn.Update.fullFunc tableName idFunc document conn - - /// Update a partial document - member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPatch) = - WithConn.Update.partialById tableName docId partial conn - - /// Update partial documents using a comparison on a JSON field - member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPatch) = - WithConn.Update.partialByField tableName fieldName op value partial 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 fieldName op (value: obj) = - WithConn.Delete.byField tableName fieldName op value conn - - -open System.Runtime.CompilerServices - -/// C# extensions on the SqliteConnection type -[] -type SqliteConnectionCSharpExtensions = - - /// Execute a query that returns a list of results - [] - static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func) = - WithConn.Custom.List<'TDoc>(query, parameters, mapFunc, conn) - - /// Execute a query that returns one or no results - [] - static member inline CustomSingle<'TDoc when 'TDoc: null>( - conn, query, parameters, mapFunc: System.Func) = - WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) - - /// Execute a query that does not return a value - [] - static member inline CustomNonQuery(conn, query, parameters) = - WithConn.Custom.nonQuery query parameters conn - - /// Execute a query that returns a scalar value - [] - static member inline CustomScalar<'T when 'T: struct>( - conn, query, parameters, mapFunc: System.Func) = - WithConn.Custom.Scalar<'T>(query, parameters, mapFunc, conn) - - /// Create a document table - [] - static member inline EnsureTable(conn, name) = - WithConn.Definition.ensureTable name conn - - /// Create an index on one or more fields in a document table - [] - static member inline EnsureIndex(conn, tableName, indexName, fields) = - WithConn.Definition.ensureIndex tableName indexName fields conn - - /// Insert a new document - [] - static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.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 - - /// 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 - [] - static member inline CountByField(conn, tableName, fieldName, op, value: obj) = - WithConn.Count.byField tableName fieldName op value 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 - [] - static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) = - WithConn.Exists.byField tableName fieldName op value conn - - /// Retrieve all documents in the given table - [] - static member inline FindAll<'TDoc>(conn, tableName) = - WithConn.Find.All<'TDoc>(tableName, 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 - [] - static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value) = - WithConn.Find.ByField<'TDoc>(tableName, fieldName, op, value, conn) - - /// Retrieve documents via a comparison on a JSON field, returning only the first result - [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) = - WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn) - - /// Update an entire document - [] - static member inline UpdateFull<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = - WithConn.Update.full tableName docId document conn - - /// Update an entire document - [] - static member inline UpdateFullFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) = - WithConn.Update.FullFunc(tableName, idFunc, doc, conn) - - /// Update a partial document - [] - static member inline UpdatePartialById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, partial: 'TPatch) = - WithConn.Update.partialById tableName docId partial conn - - /// Update partial documents using a comparison on a JSON field - [] - static member inline UpdatePartialByField<'TPatch>(conn, tableName, fieldName, op, value: obj, partial: 'TPatch) = - WithConn.Update.partialByField tableName fieldName op value partial 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 a JSON field - [] - static member inline DeleteByField(conn, tableName, fieldName, op, value: obj) = - WithConn.Delete.byField tableName fieldName op value conn diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index c70e5b0..bb60bcd 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs new file mode 100644 index 0000000..cabe980 --- /dev/null +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -0,0 +1,511 @@ +using Expecto.CSharp; +using Expecto; +using Microsoft.Data.Sqlite; +using BitBadger.Documents.Sqlite; + +namespace BitBadger.Documents.Tests.CSharp; + +using static Runner; + +/// +/// C# tests for the extensions on the SqliteConnection class +/// +public static class SqliteCSharpExtensionTests +{ + private static Task LoadDocs() => SqliteCSharpTests.LoadDocs(); + + [Tests] + public static Test Integration = + TestList("Extensions", new[] + { + TestList("CustomSingle", new[] + { + TestCase("succeeds when a row is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.CustomSingle( + $"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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.CustomSingle( + $"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("CustomList", new[] + { + 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, + 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.CustomList( + $"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("CustomNonQuery", new[] + { + TestCase("succeeds when operating on data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); + + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.CustomNonQuery( + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", + new[] { new SqliteParameter("@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(); + await using var conn = Sqlite.Configuration.DbConn(); + + var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }), + TestCase("EnsureTable succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + Func> itExists = async name => + { + var result = await conn.CustomScalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); + return result > 0L; + }; + + 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"); + Expect.isTrue(exists, "The table should now exist"); + Expect.isTrue(alsoExists, "The key index should now exist"); + }), + TestList("Insert", new[] + { + TestCase("succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + try + { + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); + } + catch (Exception) + { + // This is what is supposed to happen + } + }) + }), + TestList("Save", new[] + { + TestCase("succeeds when a document is inserted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + + await conn.Save(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before!.Id, "test", "The document is not correct"); + Expect.isNotNull(before.Sub, "There should have been a sub-document"); + Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); + Expect.equal(before.Sub.Bar, "b", "The document is not correct"); + + await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + var after = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + 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(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountAll(SqliteDb.TableName); + Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + }), + TestCase("CountByField succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + }), + TestList("ExistsById", new[] + { + TestCase("succeeds when a document exists", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsById(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + }), + TestList("ExistsByField", new[] + { + 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, "NumValue", Op.GE, 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var exists = await conn.ExistsByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }), + TestList("FindAll", new[] + { + TestCase("succeeds when there is data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + + var results = await conn.FindAll(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var results = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + }), + TestList("FindById", new[] + { + 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.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("FindByField", new[] + { + 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, "NumValue", Op.GT, 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindFirstByField", new[] + { + 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, "Value", Op.EQ, + "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, + "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Value", Op.EQ, + "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("UpdateFull", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); + var after = await conn.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFull(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + TestList("UpdateFullFunc", new[] + { + 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.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("UpdatePartialById", new[] + { + 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.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByField", new[] + { + 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.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", + new { NumValue = 77 }); + var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); + }) + }), + TestList("DeleteById", new[] + { + TestCase("succeeds when a document is deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "four"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "thirty"); + var remaining = await conn.CountAll(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByField", new[] + { + 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, "Value", Op.NE, "purple"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "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 73baea0..f1c35c6 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -56,7 +56,7 @@ public static class SqliteCSharpTests new() { Id = "five", Value = "purple", NumValue = 18 } }; - private static async Task LoadDocs() + internal static async Task LoadDocs() { foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } @@ -552,500 +552,6 @@ public static class SqliteCSharpTests }) }) }), - TestList("Extensions", new[] - { - TestList("CustomSingle", new[] - { - TestCase("succeeds when a row is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.CustomSingle( - $"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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.CustomSingle( - $"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("CustomList", new[] - { - 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, - 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var docs = await conn.CustomList( - $"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("CustomNonQuery", new[] - { - TestCase("succeeds when operating on data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); - - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.CustomNonQuery( - $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@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(); - await using var conn = Sqlite.Configuration.DbConn(); - - var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, - rdr => rdr.GetInt32(0)); - Expect.equal(nbr, 5, "The query should have returned the number 5"); - }), - TestCase("EnsureTable succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - - Func> itExists = async name => - { - var result = await conn.CustomScalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, - rdr => rdr.GetInt64(0)); - return result > 0L; - }; - - 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"); - Expect.isTrue(exists, "The table should now exist"); - Expect.isTrue(alsoExists, "The key index should now exist"); - }), - TestList("Insert", new[] - { - TestCase("succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); - await conn.Insert(SqliteDb.TableName, - new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); - var after = await conn.FindAll(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); - }), - TestCase("fails for duplicate key", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - try - { - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - Expect.isTrue(false, - "An exception should have been raised for duplicate document ID insert"); - } - catch (Exception) - { - // This is what is supposed to happen - } - }) - }), - TestList("Save", new[] - { - TestCase("succeeds when a document is inserted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); - - await conn.Save(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - var after = await conn.FindAll(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); - }), - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await conn.Insert(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - - var before = await conn.FindById(SqliteDb.TableName, "test"); - Expect.isNotNull(before, "There should have been a document returned"); - Expect.equal(before!.Id, "test", "The document is not correct"); - Expect.isNotNull(before.Sub, "There should have been a sub-document"); - Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); - Expect.equal(before.Sub.Bar, "b", "The document is not correct"); - - await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); - var after = await conn.FindById(SqliteDb.TableName, "test"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - 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(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var theCount = await conn.CountAll(SqliteDb.TableName); - Expect.equal(theCount, 5L, "There should have been 5 matching documents"); - }), - TestCase("CountByField succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); - Expect.equal(theCount, 2L, "There should have been 2 matching documents"); - }), - TestList("ExistsById", new[] - { - TestCase("succeeds when a document exists", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var exists = await conn.ExistsById(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) - }), - TestList("ExistsByField", new[] - { - 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, "NumValue", Op.GE, 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var exists = await conn.ExistsByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - }), - TestList("FindAll", new[] - { - TestCase("succeeds when there is data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); - - var results = await conn.FindAll(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var results = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) - }), - TestList("FindById", new[] - { - 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.FindById(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("FindByField", new[] - { - 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, "NumValue", Op.GT, 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var docs = await conn.FindByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("FindFirstByField", new[] - { - 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, "Value", Op.EQ, - "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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.FindFirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, - "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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.FindFirstByField(SqliteDb.TableName, "Value", Op.EQ, - "absent"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("UpdateFull", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); - var after = await conn.FindById(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 using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdateFull(SqliteDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) - }), - TestList("UpdateFullFunc", new[] - { - 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.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await conn.FindById(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - }), - TestList("UpdatePartialById", new[] - { - 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.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); - var after = await conn.FindById(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); - }) - }), - TestList("UpdatePartialByField", new[] - { - 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.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", - new { NumValue = 77 }); - var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); - }) - }), - TestList("DeleteById", new[] - { - TestCase("succeeds when a document is deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.DeleteById(SqliteDb.TableName, "four"); - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.DeleteById(SqliteDb.TableName, "thirty"); - var remaining = await conn.CountAll(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) - }), - TestList("DeleteByField", new[] - { - 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, "Value", Op.NE, "purple"); - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "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/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index 9dae8da..4792e95 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -6,7 +6,9 @@ + + @@ -16,6 +18,7 @@ + diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 9480cb0..3d185f5 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -7,7 +7,9 @@ let allTests = [ CommonTests.all CommonCSharpTests.Unit SqliteTests.all - SqliteCSharpTests.All ] + testSequenced SqliteExtensionTests.integrationTests + SqliteCSharpTests.All + testSequenced SqliteCSharpExtensionTests.Integration ] [] let main args = runTestsWithCLIArgs [] args allTests diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs new file mode 100644 index 0000000..dedf167 --- /dev/null +++ b/src/Tests/SqliteExtensionTests.fs @@ -0,0 +1,467 @@ +module SqliteExtensionTests + +open BitBadger.Documents +open BitBadger.Documents.Sqlite +open BitBadger.Documents.Tests +open Expecto +open Microsoft.Data.Sqlite +open Types + +/// Integration tests for the F# extensions on the SqliteConnection data type +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 "Extensions" [ + testTask "ensureTable succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + let itExists (name: string) = task { + let! result = + conn.customScalar + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" + [ SqliteParameter("@name", name) ] + _.GetInt64(0) + return result > 0 + } + + 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! conn.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" + } + testList "insert" [ + testTask "succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + let! before = conn.findAll 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! conn.insert SqliteDb.TableName testDoc + let! after = conn.findAll SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! conn.insert SqliteDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + conn.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() + use conn = Configuration.dbConn () + let! before = conn.findAll 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! conn.save SqliteDb.TableName testDoc + let! after = conn.findAll SqliteDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! conn.insert SqliteDb.TableName testDoc + + let! before = conn.findById SqliteDb.TableName "test" + if Option.isNone before then Expect.isTrue false "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! conn.save SqliteDb.TableName upd8Doc + let! after = conn.findById SqliteDb.TableName "test" + if Option.isNone after then + Expect.isTrue false "There should have been a document returned post-update" + Expect.equal after.Value upd8Doc "The updated document is not correct" + } + ] + testTask "countAll succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! theCount = conn.countAll SqliteDb.TableName + Expect.equal theCount 5L "There should have been 5 matching documents" + } + testTask "countByField succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! theCount = conn.countByField SqliteDb.TableName "Value" EQ "purple" + Expect.equal theCount 2L "There should have been 2 matching documents" + } + testList "existsById" [ + testTask "succeeds when a document exists" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsById 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() + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsById SqliteDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "existsByField" [ + testTask "succeeds when documents exist" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsByField SqliteDb.TableName "NumValue" EQ 10 + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! exists = conn.existsByField SqliteDb.TableName "Nothing" EQ "none" + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "findAll" [ + testTask "succeeds when there is data" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + + 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 = conn.findAll 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() + use conn = Configuration.dbConn () + let! results = conn.findAll SqliteDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "findById" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findById 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() + use conn = Configuration.dbConn () + 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" + } + ] + testList "findByField" [ + testTask "succeeds when documents are found" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = conn.findByField SqliteDb.TableName "Sub.Foo" EQ "green" + Expect.equal (List.length 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 "Value" EQ "mauve" + Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + } + ] + testList "findFirstByField" [ + testTask "succeeds when a document is found" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "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() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField SqliteDb.TableName "Sub.Foo" EQ "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() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "absent" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + testList "updateFull" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! conn.updateFull SqliteDb.TableName "one" testDoc + let! after = conn.findById SqliteDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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() + use conn = Configuration.dbConn () + + let! before = conn.findAll SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFull + SqliteDb.TableName + "test" + { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "updateFullFunc" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updateFullFunc + SqliteDb.TableName + (_.Id) + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = conn.findById SqliteDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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() + use conn = Configuration.dbConn () + + let! before = conn.findAll SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFullFunc + SqliteDb.TableName + (_.Id) + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] + testList "updatePartialById" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updatePartialById SqliteDb.TableName "one" {| NumValue = 44 |} + let! after = conn.findById SqliteDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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() + use conn = Configuration.dbConn () + + let! before = conn.findAll SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialById SqliteDb.TableName "test" {| Foo = "green" |} + } + ] + testList "updatePartialByField" [ + testTask "succeeds when a document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = conn.countByField SqliteDb.TableName "NumValue" EQ 77 + Expect.equal after 2L "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + + let! before = conn.findAll SqliteDb.TableName + Expect.isEmpty before "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + } + ] + testList "deleteById" [ + testTask "succeeds when a document is deleted" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteById SqliteDb.TableName "four" + let! remaining = conn.countAll 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() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteById SqliteDb.TableName "thirty" + let! remaining = conn.countAll SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "deleteByField" [ + testTask "succeeds when documents are deleted" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteByField SqliteDb.TableName "Value" NE "purple" + let! remaining = conn.countAll SqliteDb.TableName + Expect.equal remaining 2L "There should have been 2 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.deleteByField SqliteDb.TableName "Value" EQ "crimson" + let! remaining = conn.countAll SqliteDb.TableName + Expect.equal remaining 5L "There should have been 5 documents remaining" + } + ] + testList "customSingle" [ + testTask "succeeds when a row is found" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.customSingle + $"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() + use conn = Configuration.dbConn () + do! loadDocs () + + let! doc = + conn.customSingle + $"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 "customList" [ + testTask "succeeds when data is found" { + use! db = SqliteDb.BuildDb() + 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" + } + testTask "succeeds when data is not found" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + let! docs = + conn.customList + $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + fromData + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "customNonQuery" [ + testTask "succeeds when operating on data" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.customNonQuery $"DELETE FROM {SqliteDb.TableName}" [] + + let! remaining = conn.countAll 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() + use conn = Configuration.dbConn () + do! loadDocs () + + do! conn.customNonQuery + $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" + [ SqliteParameter("@value", 100) ] + + let! remaining = conn.countAll SqliteDb.TableName + Expect.equal remaining 5L "There should be 5 documents remaining in the table" + } + ] + testTask "customScalar succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + + let! nbr = conn.customScalar "SELECT 5 AS test_value" [] _.GetInt32(0) + Expect.equal nbr 5 "The query should have returned the number 5" + } + test "clean up database" { + Configuration.useConnectionString "data source=:memory:" + } + ] diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 56d925a..a632a35 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -1,26 +1,11 @@ module SqliteTests -type SubDocument = - { Foo: string - Bar: string } - -type JsonDocument = - { Id: string - Value: string - NumValue: int - Sub: SubDocument option } - -let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } - -/// A function that always returns true -let isTrue<'T> (_ : 'T) = true - - open BitBadger.Documents open BitBadger.Documents.Sqlite open BitBadger.Documents.Tests open Expecto open Microsoft.Data.Sqlite +open Types /// Unit tests for the SQLite library let unitTests = @@ -99,6 +84,81 @@ let integrationTests = 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 () + + 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() @@ -160,13 +220,13 @@ let integrationTests = do! insert SqliteDb.TableName testDoc let! before = Find.byId SqliteDb.TableName "test" - if Option.isNone before then Expect.isTrue false "There should have been a document returned" + 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" - if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" + Expect.isSome after "There should have been a document returned post-update" Expect.equal after.Value upd8Doc "The updated document is not correct" } ] @@ -311,15 +371,14 @@ let integrationTests = let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } do! Update.full SqliteDb.TableName "one" testDoc let! after = Find.byId SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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.hasCountOf before 0u isTrue "There should have been no documents returned" + Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test do! Update.full @@ -335,8 +394,7 @@ let integrationTests = do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } let! after = Find.byId SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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 } @@ -346,7 +404,7 @@ let integrationTests = use! db = SqliteDb.BuildDb() let! before = Find.all SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } @@ -359,15 +417,14 @@ let integrationTests = do! Update.partialById SqliteDb.TableName "one" {| NumValue = 44 |} let! after = Find.byId SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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.hasCountOf before 0u isTrue "There should have been no documents returned" + Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test do! Update.partialById SqliteDb.TableName "test" {| Foo = "green" |} @@ -386,7 +443,7 @@ let integrationTests = use! db = SqliteDb.BuildDb() let! before = Find.all SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test do! Update.partialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} @@ -431,524 +488,6 @@ let integrationTests = } ] ] - 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 isTrue "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" - } - ] - testList "Extensions" [ - testTask "ensureTable succeeds" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - let itExists (name: string) = task { - let! result = - conn.customScalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" - [ SqliteParameter("@name", name) ] - _.GetInt64(0) - return result > 0 - } - - 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! conn.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" - } - testList "insert" [ - testTask "succeeds" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - let! before = conn.findAll 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! conn.insert SqliteDb.TableName testDoc - let! after = conn.findAll SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "fails for duplicate key" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! conn.insert SqliteDb.TableName { emptyDoc with Id = "test" } - Expect.throws - (fun () -> - conn.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() - use conn = Configuration.dbConn () - let! before = conn.findAll 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! conn.save SqliteDb.TableName testDoc - let! after = conn.findAll SqliteDb.TableName - Expect.equal after [ testDoc ] "There should have been one document inserted" - } - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } - do! conn.insert SqliteDb.TableName testDoc - - let! before = conn.findById SqliteDb.TableName "test" - if Option.isNone before then Expect.isTrue false "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! conn.save SqliteDb.TableName upd8Doc - let! after = conn.findById SqliteDb.TableName "test" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" - Expect.equal after.Value upd8Doc "The updated document is not correct" - } - ] - testTask "countAll succeeds" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! theCount = conn.countAll SqliteDb.TableName - Expect.equal theCount 5L "There should have been 5 matching documents" - } - testTask "countByField succeeds" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! theCount = conn.countByField SqliteDb.TableName "Value" EQ "purple" - Expect.equal theCount 2L "There should have been 2 matching documents" - } - testList "existsById" [ - testTask "succeeds when a document exists" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! exists = conn.existsById 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() - use conn = Configuration.dbConn () - do! loadDocs () - - let! exists = conn.existsById SqliteDb.TableName "seven" - Expect.isFalse exists "There should not have been an existing document" - } - ] - testList "existsByField" [ - testTask "succeeds when documents exist" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! exists = conn.existsByField SqliteDb.TableName "NumValue" EQ 10 - Expect.isTrue exists "There should have been existing documents" - } - testTask "succeeds when no matching documents exist" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! exists = conn.existsByField SqliteDb.TableName "Nothing" EQ "none" - Expect.isFalse exists "There should not have been any existing documents" - } - ] - testList "findAll" [ - testTask "succeeds when there is data" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - - 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 = conn.findAll 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() - use conn = Configuration.dbConn () - let! results = conn.findAll SqliteDb.TableName - Expect.equal results [] "There should have been no documents returned" - } - ] - testList "findById" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = conn.findById 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() - use conn = Configuration.dbConn () - 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" - } - ] - testList "findByField" [ - testTask "succeeds when documents are found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! docs = conn.findByField SqliteDb.TableName "Sub.Foo" EQ "green" - Expect.equal (List.length 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 "Value" EQ "mauve" - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" - } - ] - testList "findFirstByField" [ - testTask "succeeds when a document is found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "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() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = conn.findFirstByField SqliteDb.TableName "Sub.Foo" EQ "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() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = conn.findFirstByField SqliteDb.TableName "Value" EQ "absent" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" - } - ] - testList "updateFull" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! conn.updateFull SqliteDb.TableName "one" testDoc - let! after = conn.findById SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "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() - use conn = Configuration.dbConn () - - let! before = conn.findAll SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - - // This not raising an exception is the test - do! conn.updateFull - SqliteDb.TableName - "test" - { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } - } - ] - testList "updateFullFunc" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.updateFullFunc - SqliteDb.TableName - (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = conn.findById SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "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() - use conn = Configuration.dbConn () - - let! before = conn.findAll SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - - // This not raising an exception is the test - do! conn.updateFullFunc - SqliteDb.TableName - (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - } - ] - testList "updatePartialById" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.updatePartialById SqliteDb.TableName "one" {| NumValue = 44 |} - let! after = conn.findById SqliteDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "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() - use conn = Configuration.dbConn () - - let! before = conn.findAll SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - - // This not raising an exception is the test - do! conn.updatePartialById SqliteDb.TableName "test" {| Foo = "green" |} - } - ] - testList "updatePartialByField" [ - testTask "succeeds when a document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} - let! after = conn.countByField SqliteDb.TableName "NumValue" EQ 77 - Expect.equal after 2L "There should have been 2 documents returned" - } - testTask "succeeds when no document is updated" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - - let! before = conn.findAll SqliteDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" - - // This not raising an exception is the test - do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} - } - ] - testList "deleteById" [ - testTask "succeeds when a document is deleted" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.deleteById SqliteDb.TableName "four" - let! remaining = conn.countAll 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() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.deleteById SqliteDb.TableName "thirty" - let! remaining = conn.countAll SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - testList "deleteByField" [ - testTask "succeeds when documents are deleted" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.deleteByField SqliteDb.TableName "Value" NE "purple" - let! remaining = conn.countAll SqliteDb.TableName - Expect.equal remaining 2L "There should have been 2 documents remaining" - } - testTask "succeeds when documents are not deleted" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.deleteByField SqliteDb.TableName "Value" EQ "crimson" - let! remaining = conn.countAll SqliteDb.TableName - Expect.equal remaining 5L "There should have been 5 documents remaining" - } - ] - testList "customSingle" [ - testTask "succeeds when a row is found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = - conn.customSingle - $"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() - use conn = Configuration.dbConn () - do! loadDocs () - - let! doc = - conn.customSingle - $"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 "customList" [ - testTask "succeeds when data is found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! docs = conn.customList (Query.selectFromTable SqliteDb.TableName) [] fromData - Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned" - } - testTask "succeeds when data is not found" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - let! docs = - conn.customList - $"SELECT data FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - fromData - Expect.isEmpty docs "There should have been no documents returned" - } - ] - testList "customNonQuery" [ - testTask "succeeds when operating on data" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.customNonQuery $"DELETE FROM {SqliteDb.TableName}" [] - - let! remaining = conn.countAll 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() - use conn = Configuration.dbConn () - do! loadDocs () - - do! conn.customNonQuery - $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value" - [ SqliteParameter("@value", 100) ] - - let! remaining = conn.countAll SqliteDb.TableName - Expect.equal remaining 5L "There should be 5 documents remaining in the table" - } - ] - testTask "customScalar succeeds" { - use! db = SqliteDb.BuildDb() - use conn = Configuration.dbConn () - - let! nbr = conn.customScalar "SELECT 5 AS test_value" [] _.GetInt32(0) - Expect.equal nbr 5 "The query should have returned the number 5" - } - ] test "clean up database" { Configuration.useConnectionString "data source=:memory:" } diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs new file mode 100644 index 0000000..e696a2a --- /dev/null +++ b/src/Tests/Types.fs @@ -0,0 +1,13 @@ +module Types + +type SubDocument = + { Foo: string + Bar: string } + +type JsonDocument = + { Id: string + Value: string + NumValue: int + Sub: SubDocument option } + +let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } -- 2.45.1 From d02ea638bc7c7493a29f3ed97c2d72cc3acda8ea Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 25 Dec 2023 23:59:49 -0500 Subject: [PATCH 10/26] Import Postgres F# functions --- src/BitBadger.Documents.sln | 6 + src/Directory.Build.props | 8 +- .../BitBadger.Documents.Postgres.fsproj | 15 + src/Postgres/Library.fs | 566 ++++++++++++++ .../BitBadger.Documents.Tests.CSharp.csproj | 2 + src/Tests.CSharp/PostgresDb.cs | 144 ++++ src/Tests/BitBadger.Documents.Tests.fsproj | 2 + src/Tests/PostgresTests.fs | 711 ++++++++++++++++++ 8 files changed, 1450 insertions(+), 4 deletions(-) create mode 100644 src/Postgres/BitBadger.Documents.Postgres.fsproj create mode 100644 src/Postgres/Library.fs create mode 100644 src/Tests.CSharp/PostgresDb.cs create mode 100644 src/Tests/PostgresTests.fs diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln index 00d5639..9381b10 100644 --- a/src/BitBadger.Documents.sln +++ b/src/BitBadger.Documents.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.C EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite.Extensions", "Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj", "{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Postgres", "Postgres\BitBadger.Documents.Postgres.fsproj", "{30E73486-9D00-440B-B4AC-5B7AC029AE72}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.Build.0 = Debug|Any CPU {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.ActiveCfg = Release|Any CPU {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.Build.0 = Release|Any CPU + {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2d3771a..4c182c4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,10 @@ net6.0;net7.0;net8.0 embedded false - 1.0.0.0 - 1.0.0.0 - 1.0.0 - alpha + 3.0.0.0 + 3.0.0.0 + 3.0.0 + rc-1 Initial release with F# support danieljsummers Bit Badger Solutions diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj new file mode 100644 index 0000000..246a808 --- /dev/null +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs new file mode 100644 index 0000000..ba7c111 --- /dev/null +++ b/src/Postgres/Library.fs @@ -0,0 +1,566 @@ +namespace BitBadger.Documents.Postgres + +/// The type of index to generate for the document +[] +type DocumentIndex = + /// A GIN index with standard operations (all operators supported) + | Full + /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) + | Optimized + + +/// Configuration for document handling +module Configuration = + + /// The data source to use for query execution + let mutable private dataSourceValue : Npgsql.NpgsqlDataSource option = None + + /// Register a data source to use for query execution (disposes the current one if it exists) + let useDataSource source = + if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose() + dataSourceValue <- Some source + + /// Retrieve the currently configured data source + let dataSource () = + match dataSourceValue with + | Some source -> source + | None -> invalidOp "Please provide a data source before attempting data access" + + +open Npgsql.FSharp + +/// Helper functions +[] +module private Helpers = + /// Shorthand to retrieve the data source as SqlProps + let internal fromDataSource () = + Configuration.dataSource () |> Sql.fromDataSource + + open System.Threading.Tasks + + /// Execute a task and ignore the result + let internal ignoreTask<'T> (it : Task<'T>) = backgroundTask { + let! _ = it + () + } + + +open BitBadger.Documents + +/// Data definition +[] +module Definition = + + /// SQL statement to create a document table + let createTable name = + $"CREATE TABLE IF NOT EXISTS %s{name} (data JSONB NOT NULL)" + + /// SQL statement to create a key index for a document table + let createKey (name : string) = + let tableName = name.Split(".") |> Array.last + $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{tableName}_key ON {name} ((data ->> '{Configuration.idField ()}'))" + + /// SQL statement to create an index on documents in the specified table + let createIndex (name : string) idxType = + let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" + let tableName = name.Split(".") |> Array.last + $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" + + /// Definitions that take SqlProps as their last parameter + module WithProps = + + /// Create a document table + let ensureTable name sqlProps = backgroundTask { + do! sqlProps |> Sql.query (createTable name) |> Sql.executeNonQueryAsync |> ignoreTask + do! sqlProps |> Sql.query (createKey name) |> Sql.executeNonQueryAsync |> ignoreTask + } + + /// Create an index on documents in the specified table + let ensureIndex name idxType sqlProps = + sqlProps |> Sql.query (createIndex name idxType) |> Sql.executeNonQueryAsync |> ignoreTask + + /// Create a document table + let ensureTable name = + WithProps.ensureTable name (fromDataSource ()) + + let ensureIndex name idxType = + WithProps.ensureIndex name idxType (fromDataSource ()) + + +/// 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}" + + /// Create a WHERE clause fragment to implement an ID-based query + let whereById paramName = + $"data ->> '{Configuration.idField ()}' = %s{paramName}" + + /// Create a WHERE clause fragment to implement a @> (JSON contains) condition + let whereDataContains paramName = + $"data @> %s{paramName}" + + /// Create a WHERE clause fragment to implement a @? (JSON Path match) condition + let whereJsonPathMatches paramName = + $"data @? %s{paramName}::jsonpath" + + /// Create a JSONB document parameter + let jsonbDocParam (it: obj) = + Sql.jsonb (Configuration.serializer().Serialize it) + + /// Create ID and data parameters for a query + let docParameters<'T> docId (doc: 'T) = + [ "@id", Sql.string docId; "@data", jsonbDocParam doc ] + + /// Query to insert a document + let insert tableName = + $"INSERT INTO %s{tableName} VALUES (@data)" + + /// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + let save tableName = + sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" + tableName (Configuration.idField ()) + + /// 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 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"}""" + + /// 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 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""" + + /// Queries for retrieving documents + module Find = + + /// Query to retrieve a document by its ID + let byId tableName = + $"""{selectFromTable tableName} WHERE {whereById "@id"}""" + + /// Query to retrieve documents using a JSON containment query (@>) + let byContains tableName = + $"""{selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" + + /// Query to retrieve documents using a JSON Path match (@?) + let byJsonPath tableName = + $"""{selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" + + /// Queries to update documents + module Update = + + /// Query to update a document + let full tableName = + $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" + + /// Query to update a document + let partialById tableName = + $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereById "@id"}""" + + /// Query to update partial documents matching a JSON containment query (@>) + let partialByContains tableName = + $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}""" + + /// Query to update partial documents matching a JSON containment query (@>) + let partialByJsonPath tableName = + $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}""" + + /// 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 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"}""" + + +/// Functions for dealing with results +[] +module Results = + + /// Create a domain item from a document, specifying the field in which the document is found + let fromDocument<'T> field (row: RowReader) : 'T = + Configuration.serializer().Deserialize<'T>(row.string field) + + /// Create a domain item from a document + let fromData<'T> row : 'T = + fromDocument "data" row + + +/// Versions of queries that accept SqlProps as the last parameter +module WithProps = + + /// Execute a non-query statement to manipulate a document + let private executeNonQuery query (document: 'T) sqlProps = + sqlProps + |> Sql.query query + |> Sql.parameters [ "@data", Query.jsonbDocParam document ] + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Execute a non-query statement to manipulate a document with an ID specified + let private executeNonQueryWithId query docId (document: 'T) sqlProps = + sqlProps + |> Sql.query query + |> Sql.parameters (Query.docParameters docId document) + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Insert a new document + let insert<'T> tableName (document: 'T) sqlProps = + executeNonQuery (Query.insert tableName) document sqlProps + + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + let save<'T> tableName (document: 'T) sqlProps = + executeNonQuery (Query.save tableName) document sqlProps + + /// Commands to count documents + [] + module Count = + + /// Count all documents in a table + let all tableName sqlProps = + sqlProps + |> Sql.query (Query.Count.all tableName) + |> Sql.executeRowAsync (fun row -> row.int "it") + + /// Count matching documents using a JSON containment query (@>) + let byContains tableName (criteria: obj) sqlProps = + sqlProps + |> Sql.query (Query.Count.byContains tableName) + |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] + |> Sql.executeRowAsync (fun row -> row.int "it") + + /// Count matching documents using a JSON Path match query (@?) + let byJsonPath tableName jsonPath sqlProps = + sqlProps + |> Sql.query (Query.Count.byJsonPath tableName) + |> Sql.parameters [ "@path", Sql.string jsonPath ] + |> Sql.executeRowAsync (fun row -> row.int "it") + + /// Commands to determine if documents exist + [] + module Exists = + + /// Determine if a document exists for the given ID + let byId tableName docId sqlProps = + sqlProps + |> Sql.query (Query.Exists.byId tableName) + |> Sql.parameters [ "@id", Sql.string docId ] + |> Sql.executeRowAsync (fun row -> row.bool "it") + + /// Determine if a document exists using a JSON containment query (@>) + let byContains tableName (criteria: obj) sqlProps = + sqlProps + |> Sql.query (Query.Exists.byContains tableName) + |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] + |> Sql.executeRowAsync (fun row -> row.bool "it") + + /// Determine if a document exists using a JSON Path match query (@?) + let byJsonPath tableName jsonPath sqlProps = + sqlProps + |> Sql.query (Query.Exists.byJsonPath tableName) + |> Sql.parameters [ "@path", Sql.string jsonPath ] + |> Sql.executeRowAsync (fun row -> row.bool "it") + + /// Commands to determine if documents exist + [] + module Find = + + /// Retrieve all documents in the given table + let all<'T> tableName sqlProps = + sqlProps + |> Sql.query (Query.selectFromTable tableName) + |> Sql.executeAsync fromData<'T> + + /// Retrieve a document by its ID + let byId<'T> tableName docId sqlProps = backgroundTask { + let! results = + sqlProps + |> Sql.query (Query.Find.byId tableName) + |> Sql.parameters [ "@id", Sql.string docId ] + |> Sql.executeAsync fromData<'T> + return List.tryHead results + } + + /// Execute a JSON containment query (@>) + let byContains<'T> tableName (criteria: obj) sqlProps = + sqlProps + |> Sql.query (Query.Find.byContains tableName) + |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] + |> Sql.executeAsync fromData<'T> + + /// Execute a JSON Path match query (@?) + let byJsonPath<'T> tableName jsonPath sqlProps = + sqlProps + |> Sql.query (Query.Find.byJsonPath tableName) + |> Sql.parameters [ "@path", Sql.string jsonPath ] + |> Sql.executeAsync fromData<'T> + + /// Execute a JSON containment query (@>), returning only the first result + let firstByContains<'T> tableName (criteria: obj) sqlProps = backgroundTask { + let! results = byContains<'T> tableName criteria sqlProps + return List.tryHead results + } + + /// Execute a JSON Path match query (@?), returning only the first result + let firstByJsonPath<'T> tableName jsonPath sqlProps = backgroundTask { + let! results = byJsonPath<'T> tableName jsonPath sqlProps + return List.tryHead results + } + + /// Commands to update documents + [] + module Update = + + /// Update an entire document + let full<'T> tableName docId (document: 'T) sqlProps = + executeNonQueryWithId (Query.Update.full tableName) docId document sqlProps + + /// Update an entire document + let fullFunc<'T> tableName (idFunc: 'T -> string) (document: 'T) sqlProps = + full tableName (idFunc document) document sqlProps + + /// Update a partial document + let partialById tableName docId (partial: obj) sqlProps = + executeNonQueryWithId (Query.Update.partialById tableName) docId partial sqlProps + + /// Update partial documents using a JSON containment query in the WHERE clause (@>) + let partialByContains tableName (criteria: obj) (partial: obj) sqlProps = + sqlProps + |> Sql.query (Query.Update.partialByContains tableName) + |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@criteria", Query.jsonbDocParam criteria ] + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Update partial documents using a JSON Path match query in the WHERE clause (@?) + let partialByJsonPath tableName jsonPath (partial: obj) sqlProps = + sqlProps + |> Sql.query (Query.Update.partialByJsonPath tableName) + |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@path", Sql.string jsonPath ] + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Commands to delete documents + [] + module Delete = + + /// Delete a document by its ID + let byId tableName docId sqlProps = + executeNonQueryWithId (Query.Delete.byId tableName) docId {||} sqlProps + + /// Delete documents by matching a JSON contains query (@>) + let byContains tableName (criteria: obj) sqlProps = + sqlProps + |> Sql.query (Query.Delete.byContains tableName) + |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Delete documents by matching a JSON Path match query (@?) + let byJsonPath tableName path sqlProps = + sqlProps + |> Sql.query (Query.Delete.byJsonPath tableName) + |> Sql.parameters [ "@path", Sql.string path ] + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Commands to execute custom SQL queries + [] + module Custom = + + /// Execute a query that returns one or no results + let single<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = backgroundTask { + let! results = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeAsync mapFunc + return List.tryHead results + } + + /// Execute a query that returns a list of results + let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeAsync mapFunc + + /// Execute a query that returns no results + let nonQuery query parameters sqlProps = + Sql.query query sqlProps + |> Sql.parameters (List.ofSeq parameters) + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Execute a query that returns a scalar value + let scalar<'T when 'T : struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeRowAsync mapFunc + + +/// Document writing functions +[] +module Document = + /// Insert a new document + let insert<'T> tableName (document: 'T) = + WithProps.insert tableName document (fromDataSource ()) + + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + let save<'T> tableName (document: 'T) = + WithProps.save<'T> tableName document (fromDataSource ()) + + +/// Queries to count documents +[] +module Count = + + /// Count all documents in a table + let all tableName = + WithProps.Count.all tableName (fromDataSource ()) + + /// Count matching documents using a JSON containment query (@>) + let byContains tableName criteria = + WithProps.Count.byContains tableName criteria (fromDataSource ()) + + /// Count matching documents using a JSON Path match query (@?) + let byJsonPath tableName jsonPath = + WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ()) + + +/// Queries to determine if documents exist +[] +module Exists = + + /// Determine if a document exists for the given ID + let byId tableName docId = + WithProps.Exists.byId tableName docId (fromDataSource ()) + + /// Determine if a document exists using a JSON containment query (@>) + let byContains tableName criteria = + WithProps.Exists.byContains tableName criteria (fromDataSource ()) + + /// Determine if a document exists using a JSON Path match query (@?) + let byJsonPath tableName jsonPath = + WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ()) + + +/// Commands to retrieve documents +[] +module Find = + + /// Retrieve all documents in the given table + let all<'T> tableName = + WithProps.Find.all<'T> tableName (fromDataSource ()) + + /// Retrieve a document by its ID + let byId<'T> tableName docId = + WithProps.Find.byId<'T> tableName docId (fromDataSource ()) + + /// Execute a JSON containment query (@>) + let byContains<'T> tableName criteria = + WithProps.Find.byContains<'T> tableName criteria (fromDataSource ()) + + /// Execute a JSON Path match query (@?) + let byJsonPath<'T> tableName jsonPath = + WithProps.Find.byJsonPath<'T> tableName jsonPath (fromDataSource ()) + + /// Execute a JSON containment query (@>), returning only the first result + let firstByContains<'T> tableName (criteria: obj) = + WithProps.Find.firstByContains<'T> tableName criteria (fromDataSource ()) + + /// Execute a JSON Path match query (@?), returning only the first result + let firstByJsonPath<'T> tableName jsonPath = + WithProps.Find.firstByJsonPath<'T> tableName jsonPath (fromDataSource ()) + + +/// Commands to update documents +[] +module Update = + + /// Update a full document + let full<'T> tableName docId (document: 'T) = + WithProps.Update.full<'T> tableName docId document (fromDataSource ()) + + /// Update a full document + let fullFunc<'T> tableName idFunc (document: 'T) = + WithProps.Update.fullFunc<'T> tableName idFunc document (fromDataSource ()) + + /// Update a partial document + let partialById tableName docId (partial: obj) = + WithProps.Update.partialById tableName docId partial (fromDataSource ()) + + /// Update partial documents using a JSON containment query in the WHERE clause (@>) + let partialByContains tableName (criteria: obj) (partial: obj) = + WithProps.Update.partialByContains tableName criteria partial (fromDataSource ()) + + /// Update partial documents using a JSON Path match query in the WHERE clause (@?) + let partialByJsonPath tableName jsonPath (partial: obj) = + WithProps.Update.partialByJsonPath tableName jsonPath partial (fromDataSource ()) + + +/// Commands to delete documents +[] +module Delete = + + /// Delete a document by its ID + let byId tableName docId = + WithProps.Delete.byId tableName docId (fromDataSource ()) + + /// Delete documents by matching a JSON contains query (@>) + let byContains tableName (criteria: obj) = + WithProps.Delete.byContains tableName criteria (fromDataSource ()) + + /// Delete documents by matching a JSON Path match query (@?) + let byJsonPath tableName path = + WithProps.Delete.byJsonPath tableName path (fromDataSource ()) + + +/// Commands to execute custom SQL queries +[] +module Custom = + + /// Execute a query that returns one or no results + let single<'T> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.single query parameters mapFunc (fromDataSource ()) + + /// Execute a query that returns a list of results + let list<'T> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.list query parameters mapFunc (fromDataSource ()) + + /// Execute a query that returns no results + let nonQuery query parameters = + WithProps.Custom.nonQuery query parameters (fromDataSource ()) + + /// Execute a query that returns a scalar value + let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.scalar query parameters mapFunc (fromDataSource ()) diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index bb60bcd..dad9a03 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -7,12 +7,14 @@ + + diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs new file mode 100644 index 0000000..ce2ec6a --- /dev/null +++ b/src/Tests.CSharp/PostgresDb.cs @@ -0,0 +1,144 @@ +using BitBadger.Documents.Postgres; +using Npgsql; +using Npgsql.FSharp; +using ThrowawayDb.Postgres; + +namespace BitBadger.Documents.Tests; + +/// +/// A throwaway SQLite database file, which will be deleted when it goes out of scope +/// +public class ThrowawayPostgresDb : IDisposable, IAsyncDisposable +{ + private readonly ThrowawayDatabase _db; + + public string ConnectionString => _db.ConnectionString; + + public ThrowawayPostgresDb(ThrowawayDatabase db) + { + _db = db; + } + + public void Dispose() + { + _db.Dispose(); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + _db.Dispose(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } +} + +/// +/// Database helpers for PostgreSQL integration tests +/// +public static class PostgresDb +{ + /// + /// The name of the table used for testing + /// + public const string TableName = "test_table"; + + /// + /// The host for the database + /// + private static readonly Lazy DbHost = new(() => + { + return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbHost") switch + { + null => "localhost", + var host when host.Trim() == "" => "localhost", + var host => host + }; + }); + + /// + /// The port for the database + /// + private static readonly Lazy DbPort = new(() => + { + return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbPort") switch + { + null => 5432, + var port when port.Trim() == "" => 5432, + var port => int.Parse(port) + }; + }); + + /// + /// The database itself + /// + private static readonly Lazy DbDatabase = new(() => + { + return Environment.GetEnvironmentVariable("BitBadger.Documents.Postres.DbDatabase") switch + { + null => "postgres", + var db when db.Trim() == "" => "postgres", + var db => db + }; + }); + + /// + /// The user to use in connecting to the database + /// + private static readonly Lazy DbUser = new(() => + { + return Environment.GetEnvironmentVariable("BitBadger.Documents.Postgres.DbUser") switch + { + null => "postgres", + var user when user.Trim() == "" => "postgres", + var user => user + }; + }); + + /// + /// The password to use for the database + /// + private static readonly Lazy DbPassword = new(() => + { + return Environment.GetEnvironmentVariable("BitBadger.Documents.Postrgres.DbPwd") switch + { + null => "postgres", + var pwd when pwd.Trim() == "" => "postgres", + var pwd => pwd + }; + }); + + /// + /// The overall connection string + /// + public static readonly Lazy ConnStr = new(() => + Sql.formatConnectionString( + Sql.password(DbPassword.Value, + Sql.username(DbUser.Value, + Sql.database(DbDatabase.Value, + Sql.port(DbPort.Value, + Sql.host(DbHost.Value))))))); + + /// + /// Create a data source using the derived connection string + /// + public static NpgsqlDataSource MkDataSource(string cStr) => + new NpgsqlDataSourceBuilder(cStr).Build(); + + /// + /// Build the throwaway database + /// + public static ThrowawayPostgresDb BuildDb() + { + var database = ThrowawayDatabase.Create(ConnStr.Value); + + var sqlProps = Sql.connect(database.ConnectionString); + + Sql.executeNonQuery(Sql.query(Definition.createTable(TableName), sqlProps)); + Sql.executeNonQuery(Sql.query(Definition.createKey(TableName), sqlProps)); + + Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString)); + + return new ThrowawayPostgresDb(database); + } +} diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index 4792e95..a189c86 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -7,6 +7,7 @@ + @@ -18,6 +19,7 @@ + diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs new file mode 100644 index 0000000..0a7f558 --- /dev/null +++ b/src/Tests/PostgresTests.fs @@ -0,0 +1,711 @@ +module PostgresTests + +open Expecto +open BitBadger.Documents.Postgres +open BitBadger.Documents.Tests + +/// Tests which do not hit the database +let unitTests = + testList "Unit" [ + testList "Definition" [ + test "createTable succeeds" { + Expect.equal (Definition.createTable PostgresDb.TableName) + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" + "CREATE TABLE statement not constructed correctly" + } + test "createKey succeeds" { + Expect.equal (Definition.createKey PostgresDb.TableName) + $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{PostgresDb.TableName}_key ON {PostgresDb.TableName} ((data ->> 'Id'))" + "CREATE INDEX for key statement not constructed correctly" + } + test "createIndex succeeds for full index" { + Expect.equal (Definition.createIndex "schema.tbl" Full) + "CREATE INDEX IF NOT EXISTS idx_tbl ON schema.tbl USING GIN (data)" + "CREATE INDEX statement not constructed correctly" + } + test "createIndex succeeds for JSONB Path Ops index" { + Expect.equal (Definition.createIndex PostgresDb.TableName Optimized) + $"CREATE INDEX IF NOT EXISTS idx_{PostgresDb.TableName} ON {PostgresDb.TableName} USING GIN (data jsonb_path_ops)" + "CREATE INDEX statement not constructed correctly" + } + ] + testList "Query" [ + test "selectFromTable succeeds" { + Expect.equal (Query.selectFromTable PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName}" + "SELECT statement not correct" + } + test "whereById succeeds" { + Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause 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" + } + test "jsonbDocParam succeeds" { + Expect.equal (Query.jsonbDocParam {| Hello = "There" |}) (Sql.jsonb "{\"Hello\":\"There\"}") + "JSONB document not serialized correctly" + } + test "docParameters succeeds" { + let parameters = Query.docParameters "abc123" {| Testing = 456 |} + let expected = [ + "@id", Sql.string "abc123" + "@data", Sql.jsonb "{\"Testing\":456}" + ] + Expect.equal parameters expected "There should have been 2 parameters, one string and one JSONB" + } + test "insert succeeds" { + Expect.equal (Query.insert PostgresDb.TableName) $"INSERT INTO {PostgresDb.TableName} VALUES (@data)" + "INSERT statement not correct" + } + test "save succeeds" { + Expect.equal (Query.save PostgresDb.TableName) + $"INSERT INTO {PostgresDb.TableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" + "INSERT ON CONFLICT UPDATE statement 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 "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 "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 "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 "Update" [ + test "full succeeds" { + Expect.equal (Query.Update.full PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = @data WHERE data ->> 'Id' = @id" + "UPDATE full statement not correct" + } + test "partialById succeeds" { + Expect.equal (Query.Update.partialById PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" + "UPDATE partial by ID statement not correct" + } + test "partialByContains succeeds" { + Expect.equal (Query.Update.partialByContains PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" + "UPDATE partial by JSON containment statement not correct" + } + test "partialByJsonPath succeeds" { + Expect.equal (Query.Update.partialByJsonPath PostgresDb.TableName) + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" + "UPDATE partial by JSON Path statement 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 "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" + } + ] + ] + ] + +open Npgsql.FSharp +open ThrowawayDb.Postgres +open Types + +let isTrue<'T> (_ : 'T) = true + +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 + } + 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 "Definition" [ + testTask "ensureTable succeeds" { + use db = PostgresDb.BuildDb() + let tableExists () = + Sql.connect db.ConnectionString + |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" + |> Sql.executeRowAsync (fun row -> row.bool "it") + let keyExists () = + Sql.connect db.ConnectionString + |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" + |> Sql.executeRowAsync (fun row -> row.bool "it") + + 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 "ensureIndex succeeds" { + use db = PostgresDb.BuildDb() + let indexExists () = + Sql.connect db.ConnectionString + |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured') AS it" + |> Sql.executeRowAsync (fun row -> row.bool "it") + + let! exists = indexExists () + Expect.isFalse exists "The index should not exist already" + + do! Definition.ensureTable "ensured" + do! Definition.ensureIndex "ensured" Optimized + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + // TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence + } + ] + 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" + if Option.isNone before then Expect.isTrue false "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" + if Option.isNone after then Expect.isTrue false "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 "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 "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.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 = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" + Expect.isFalse (Option.isSome doc) "There should not have been a document 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.isTrue (List.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.isTrue (List.isEmpty docs) "There should have been no documents 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.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 = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| 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 = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} + Expect.isFalse (Option.isSome 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.isTrue (Option.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.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 = PostgresDb.BuildDb() + do! loadDocs () + + let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" + Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + } + ] + ] + testList "Update" [ + testList "full" [ + 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.full PostgresDb.TableName "one" testDoc + let! after = Find.byId PostgresDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Find.all PostgresDb.TableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.full PostgresDb.TableName "test" + { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "fullFunc" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.fullFunc PostgresDb.TableName (_.Id) + { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = Find.byId PostgresDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Find.all PostgresDb.TableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.fullFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] + testList "partialById" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.partialById PostgresDb.TableName "one" {| NumValue = 44 |} + let! after = Find.byId PostgresDb.TableName "one" + if Option.isNone after then + Expect.isTrue false "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 = Find.all PostgresDb.TableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.partialById PostgresDb.TableName "test" {| Foo = "green" |} + } + ] + testList "partialByContains" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.partialByContains 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 = Find.all PostgresDb.TableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + } + ] + testList "partialByJsonPath" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.partialByJsonPath 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 = Find.all PostgresDb.TableName + Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + + // This not raising an exception is the test + do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + } + ] + ] + 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 "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" + } + ] + ] + testList "Custom" [ + 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 "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 "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" + } + ] + ] + |> testSequenced + + +let all = testList "FSharp.Documents" [ unitTests; integrationTests ] -- 2.45.1 From 8d3385b70bfeded2935107d0087b92b6a4d0ef17 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 12:54:37 -0500 Subject: [PATCH 11/26] Integrate PostgreSQL with common lib - Add "byField" functions to Postgres impl --- src/Common/Library.fs | 12 - src/Postgres/Library.fs | 635 +++++++++++++++++--------- src/Sqlite/Library.fs | 15 + src/Tests.CSharp/CommonCSharpTests.cs | 113 ++--- src/Tests.CSharp/SqliteCSharpTests.cs | 23 + src/Tests/CommonTests.fs | 12 - src/Tests/PostgresTests.fs | 57 +-- src/Tests/Program.fs | 1 + src/Tests/SqliteTests.fs | 22 + 9 files changed, 549 insertions(+), 341 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 7123a52..0c85cce 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -210,18 +210,6 @@ module Query = let full tableName = $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Query to update a partial document by its ID - [] - let partialById tableName = - $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {whereById "@id"}""" - - /// Query to update a partial document via a comparison on a JSON field - [] - let partialByField tableName fieldName op = - sprintf - "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" - tableName (whereByField fieldName op "@field") - /// Queries to delete documents module Delete = diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index ba7c111..18ba007 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -1,5 +1,7 @@ namespace BitBadger.Documents.Postgres +open Npgsql + /// The type of index to generate for the document [] type DocumentIndex = @@ -91,14 +93,20 @@ module Definition = [] module Query = - /// Create a SELECT clause to retrieve the document data from the given table - let selectFromTable tableName = - $"SELECT data FROM %s{tableName}" - - /// Create a WHERE clause fragment to implement an ID-based query - let whereById paramName = - $"data ->> '{Configuration.idField ()}' = %s{paramName}" - + module Definition = + + /// SQL statement to create a document table + [] + let ensureTable name = + Query.Definition.ensureTableFor name "JSONB" + + /// SQL statement to create an index on JSON documents in the specified table + [] + let ensureJsonIndex (name : string) idxType = + let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" + let tableName = name.Split '.' |> Array.last + $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" + /// Create a WHERE clause fragment to implement a @> (JSON contains) condition let whereDataContains paramName = $"data @> %s{paramName}" @@ -127,10 +135,6 @@ module Query = /// 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 JSON containment query (@>) let byContains tableName = $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" @@ -142,10 +146,6 @@ module Query = /// 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 JSON containment query (@>) let byContains tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it""" @@ -157,28 +157,24 @@ module Query = /// Queries for retrieving documents module Find = - /// Query to retrieve a document by its ID - let byId tableName = - $"""{selectFromTable tableName} WHERE {whereById "@id"}""" - /// Query to retrieve documents using a JSON containment query (@>) let byContains tableName = - $"""{selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" + $"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" /// Query to retrieve documents using a JSON Path match (@?) let byJsonPath tableName = - $"""{selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" + $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" /// Queries to update documents module Update = - /// Query to update a document - let full tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Query to update a document let partialById tableName = - $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereById "@id"}""" + $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}""" + + /// Query to update a document + let partialByField tableName fieldName op = + $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}""" /// Query to update partial documents matching a JSON containment query (@>) let partialByContains tableName = @@ -191,10 +187,6 @@ module Query = /// 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 JSON containment query (@>) let byContains tableName = $"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" @@ -204,6 +196,31 @@ module Query = $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" +/// Functions for creating parameters +[] +module Parameters = + + /// Create an ID parameter (name "@id", key will be treated as a string) + [] + let idParam (key: 'TKey) = + "@id", Sql.string (string key) + + /// 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 (name "@field") + [] + let fieldParam (value: obj) = + "@field", Sql.parameter (NpgsqlParameter("@field", value)) + + /// An empty parameter sequence + [] + let noParams = + Seq.empty + + /// Functions for dealing with results [] module Results = @@ -215,11 +232,69 @@ module Results = /// Create a domain item from a document let fromData<'T> row : 'T = fromDocument "data" row + + /// Extract a count from the column "it" + let toCount (row: RowReader) = + row.int "it" + + /// Extract a true/false value from the column "it" + let toExists (row: RowReader) = + row.bool "it" /// Versions of queries that accept SqlProps as the last parameter module WithProps = + /// Commands to execute custom SQL queries + [] + module Custom = + + /// Execute a query that returns a list of results + [] + let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeAsync mapFunc + + /// Execute a query that returns a list of results + let List<'T>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { + let! results = list query (List.ofSeq parameters) mapFunc.Invoke sqlProps + return ResizeArray results + } + + /// Execute a query that returns one or no results (returns None if not found) + [] + let single<'T> query parameters mapFunc sqlProps = backgroundTask { + let! results = list<'T> query parameters mapFunc sqlProps + return FSharp.Collections.List.tryHead results + } + + /// Execute a query that returns one or no results (returns null if not found) + let Single<'T when 'T: null>( + query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { + let! result = single<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + return Option.toObj result + } + + /// Execute a query that returns no results + [] + let nonQuery query parameters sqlProps = + Sql.query query sqlProps + |> Sql.parameters (FSharp.Collections.List.ofSeq parameters) + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Execute a query that returns a scalar value + [] + let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters 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 + /// Execute a non-query statement to manipulate a document let private executeNonQuery query (document: 'T) sqlProps = sqlProps @@ -236,208 +311,283 @@ module WithProps = |> Sql.executeNonQueryAsync |> ignoreTask - /// Insert a new document - let insert<'T> tableName (document: 'T) sqlProps = - executeNonQuery (Query.insert tableName) document sqlProps + /// Commands to add documents + [] + module Document = + + /// Insert a new document + [] + let insert<'TDoc> tableName (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps - /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - let save<'T> tableName (document: 'T) sqlProps = - executeNonQuery (Query.save tableName) document sqlProps + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save<'TDoc> tableName (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] sqlProps /// Commands to count documents [] module Count = /// Count all documents in a table + [] let all tableName sqlProps = - sqlProps - |> Sql.query (Query.Count.all tableName) - |> Sql.executeRowAsync (fun row -> row.int "it") + Custom.scalar (Query.Count.all tableName) [] toCount sqlProps + + /// Count matching documents using a JSON field comparison (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] toCount sqlProps /// Count matching documents using a JSON containment query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Count.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeRowAsync (fun row -> row.int "it") + [] + let byContains tableName (criteria: 'TContains) sqlProps = + Custom.scalar (Query.Count.byContains tableName) [ jsonParam "@criteria" criteria ] toCount sqlProps /// Count matching documents using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Count.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeRowAsync (fun row -> row.int "it") + Custom.scalar (Query.Count.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toCount sqlProps /// Commands to determine if documents exist [] module Exists = /// Determine if a document exists for the given ID - let byId tableName docId sqlProps = - sqlProps - |> Sql.query (Query.Exists.byId tableName) - |> Sql.parameters [ "@id", Sql.string docId ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + [] + let byId tableName (docId: 'TKey) sqlProps = + Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps + /// Determine if a document exists using a JSON field comparison (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] toExists sqlProps + /// Determine if a document exists using a JSON containment query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Exists.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + [] + let byContains tableName (criteria: 'TContains) sqlProps = + Custom.scalar (Query.Exists.byContains tableName) [ jsonParam "@criteria" criteria ] toExists sqlProps /// Determine if a document exists using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Exists.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + Custom.scalar (Query.Exists.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toExists sqlProps /// Commands to determine if documents exist [] module Find = /// Retrieve all documents in the given table - let all<'T> tableName sqlProps = - sqlProps - |> Sql.query (Query.selectFromTable tableName) - |> Sql.executeAsync fromData<'T> + [] + let all<'TDoc> tableName sqlProps = + Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps - /// Retrieve a document by its ID - let byId<'T> tableName docId sqlProps = backgroundTask { - let! results = - sqlProps - |> Sql.query (Query.Find.byId tableName) - |> Sql.parameters [ "@id", Sql.string docId ] - |> Sql.executeAsync fromData<'T> - return List.tryHead results - } + /// Retrieve all documents in the given table + let All<'TDoc>(tableName, sqlProps) = + Custom.List<'TDoc>(Query.selectFromTable tableName, [], fromData<'TDoc>, sqlProps) - /// Execute a JSON containment query (@>) - let byContains<'T> tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Find.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeAsync fromData<'T> + /// 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 - /// Execute a JSON Path match query (@?) - let byJsonPath<'T> tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Find.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeAsync fromData<'T> + /// 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) + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let byField<'TDoc> tableName fieldName op (value: obj) sqlProps = + Custom.list<'TDoc> (Query.Find.byField tableName fieldName op) [ fieldParam value ] fromData<'TDoc> sqlProps - /// Execute a JSON containment query (@>), returning only the first result - let firstByContains<'T> tableName (criteria: obj) sqlProps = backgroundTask { - let! results = byContains<'T> tableName criteria sqlProps - return List.tryHead results - } + /// Retrieve documents matching a JSON field comparison (->> =) + let ByField<'TDoc>(tableName, fieldName, op, value: obj, sqlProps) = + Custom.List<'TDoc>( + Query.Find.byField tableName fieldName op, [ fieldParam value ], 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 - /// Execute a JSON Path match query (@?), returning only the first result - let firstByJsonPath<'T> tableName jsonPath sqlProps = backgroundTask { - let! results = byJsonPath<'T> tableName jsonPath sqlProps - return List.tryHead results - } + /// 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) + + /// 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 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 fieldName op (value: obj) sqlProps = + Custom.single<'TDoc> + $"{Query.Find.byField tableName fieldName op} LIMIT 1" [ fieldParam value ] fromData<'TDoc> sqlProps + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.Find.byField tableName fieldName op} LIMIT 1", [ fieldParam value ], 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 + + /// 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", + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found + [] + let firstByJsonPath<'TDoc> tableName jsonPath sqlProps = + Custom.single<'TDoc> + $"{Query.Find.byJsonPath 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", + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) /// Commands to update documents [] module Update = /// Update an entire document - let full<'T> tableName docId (document: 'T) sqlProps = - executeNonQueryWithId (Query.Update.full tableName) docId document sqlProps + [] + let full tableName (docId: 'TKey) (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.Update.full tableName) [ idParam docId; jsonParam "@data" document ] sqlProps /// Update an entire document - let fullFunc<'T> tableName (idFunc: 'T -> string) (document: 'T) sqlProps = + [] + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps = full tableName (idFunc document) document sqlProps + /// Update an entire document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) = + fullFunc tableName idFunc.Invoke document sqlProps + /// Update a partial document - let partialById tableName docId (partial: obj) sqlProps = - executeNonQueryWithId (Query.Update.partialById tableName) docId partial sqlProps + [] + let partialById tableName (docId: 'TKey) (partial: 'TPartial) sqlProps = + Custom.nonQuery (Query.Update.partialById tableName) [ idParam docId; jsonParam "@data" partial ] sqlProps + + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByField tableName fieldName op) + [ jsonParam "@data" partial; fieldParam value ] + sqlProps /// Update partial documents using a JSON containment query in the WHERE clause (@>) - let partialByContains tableName (criteria: obj) (partial: obj) sqlProps = - sqlProps - |> Sql.query (Query.Update.partialByContains tableName) - |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let partialByContains tableName (criteria: 'TContains) (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByContains tableName) + [ jsonParam "@data" partial; jsonParam "@criteria" criteria ] + sqlProps /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - let partialByJsonPath tableName jsonPath (partial: obj) sqlProps = - sqlProps - |> Sql.query (Query.Update.partialByJsonPath tableName) - |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@path", Sql.string jsonPath ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let partialByJsonPath tableName jsonPath (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByJsonPath tableName) + [ jsonParam "@data" partial; "@path", Sql.string jsonPath ] + sqlProps /// Commands to delete documents [] module Delete = /// Delete a document by its ID - let byId tableName docId sqlProps = - executeNonQueryWithId (Query.Delete.byId tableName) docId {||} sqlProps + [] + let byId tableName (docId: 'TKey) sqlProps = + Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] sqlProps + /// Delete documents by matching a JSON contains query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Delete.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let byContains tableName (criteria: 'TCriteria) sqlProps = + Custom.nonQuery (Query.Delete.byContains tableName) [ jsonParam "@criteria" criteria ] sqlProps /// Delete documents by matching a JSON Path match query (@?) + [] let byJsonPath tableName path sqlProps = - sqlProps - |> Sql.query (Query.Delete.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string path ] - |> Sql.executeNonQueryAsync - |> ignoreTask + Custom.nonQuery (Query.Delete.byJsonPath tableName) [ "@path", Sql.string path ] sqlProps - /// Commands to execute custom SQL queries - [] - module Custom = + +/// Commands to execute custom SQL queries +[] +module Custom = - /// Execute a query that returns one or no results - let single<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = backgroundTask { - let! results = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeAsync mapFunc - return List.tryHead results - } + /// Execute a query that returns a list of results + [] + let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.list<'TDoc> query parameters mapFunc (fromDataSource ()) - /// Execute a query that returns a list of results - let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeAsync mapFunc + /// Execute a query that returns a list of results + let List<'TDoc>(query, parameters, mapFunc: System.Func) = + WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, fromDataSource ()) - /// Execute a query that returns no results - let nonQuery query parameters sqlProps = - Sql.query query sqlProps - |> Sql.parameters (List.ofSeq parameters) - |> Sql.executeNonQueryAsync - |> ignoreTask - - /// Execute a query that returns a scalar value - let scalar<'T when 'T : struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeRowAsync mapFunc + /// Execute a query that returns one or no results; returns None if not found + [] + let single<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ()) + + /// 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) = + WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ()) + + /// Execute a query that returns no results + [] + let nonQuery query parameters = + WithProps.Custom.nonQuery query parameters (fromDataSource ()) + + /// Execute a query that returns a scalar value + [] + let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.scalar query parameters mapFunc (fromDataSource ()) + + /// Execute a query that returns a scalar value + let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func) = + WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ()) /// Document writing functions [] module Document = + /// Insert a new document - let insert<'T> tableName (document: 'T) = - WithProps.insert tableName document (fromDataSource ()) + [] + let insert<'TDoc> tableName (document: 'TDoc) = + WithProps.Document.insert<'TDoc> tableName document (fromDataSource ()) /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - let save<'T> tableName (document: 'T) = - WithProps.save<'T> tableName document (fromDataSource ()) + [] + let save<'TDoc> tableName (document: 'TDoc) = + WithProps.Document.save<'TDoc> tableName document (fromDataSource ()) /// Queries to count documents @@ -445,14 +595,22 @@ module Document = module Count = /// Count all documents in a table + [] let all tableName = WithProps.Count.all tableName (fromDataSource ()) + /// Count matching documents using a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Count.byField tableName fieldName op value (fromDataSource ()) + /// Count matching documents using a JSON containment query (@>) + [] let byContains tableName criteria = WithProps.Count.byContains tableName criteria (fromDataSource ()) /// Count matching documents using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath = WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ()) @@ -462,14 +620,22 @@ module Count = module Exists = /// Determine if a document exists for the given ID + [] let byId tableName docId = WithProps.Exists.byId tableName docId (fromDataSource ()) - /// Determine if a document exists using a JSON containment query (@>) + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Exists.byField tableName fieldName op value (fromDataSource ()) + + /// Determine if documents exist using a JSON containment query (@>) + [] let byContains tableName criteria = WithProps.Exists.byContains tableName criteria (fromDataSource ()) - /// Determine if a document exists using a JSON Path match query (@?) + /// Determine if documents exist using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath = WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ()) @@ -479,28 +645,76 @@ module Exists = module Find = /// Retrieve all documents in the given table - let all<'T> tableName = - WithProps.Find.all<'T> tableName (fromDataSource ()) + [] + let all<'TDoc> tableName = + WithProps.Find.all<'TDoc> tableName (fromDataSource ()) - /// Retrieve a document by its ID - let byId<'T> tableName docId = - WithProps.Find.byId<'T> tableName docId (fromDataSource ()) + /// Retrieve all documents in the given table + let All<'TDoc> tableName = + WithProps.Find.All<'TDoc>(tableName, fromDataSource ()) - /// Execute a JSON containment query (@>) - let byContains<'T> tableName criteria = - WithProps.Find.byContains<'T> tableName criteria (fromDataSource ()) + /// Retrieve a document by its ID; returns None if not found + [] + let byId<'TKey, 'TDoc> tableName docId = + WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ()) - /// Execute a JSON Path match query (@?) - let byJsonPath<'T> tableName jsonPath = - WithProps.Find.byJsonPath<'T> tableName jsonPath (fromDataSource ()) + /// Retrieve a document by its ID; returns null if not found + let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey) = + WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + let byField<'TDoc> tableName fieldName op (value: obj) = + WithProps.Find.byField<'TDoc> tableName fieldName op value (fromDataSource ()) - /// Execute a JSON containment query (@>), returning only the first result - let firstByContains<'T> tableName (criteria: obj) = - WithProps.Find.firstByContains<'T> tableName criteria (fromDataSource ()) + /// Retrieve documents matching a JSON field comparison query (->> =) + let ByField<'TDoc>(tableName, fieldName, op, value: obj) = + WithProps.Find.ByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ()) + + /// Retrieve documents matching a JSON containment query (@>) + [] + let byContains<'TDoc> tableName (criteria: obj) = + WithProps.Find.byContains<'TDoc> tableName criteria (fromDataSource ()) - /// Execute a JSON Path match query (@?), returning only the first result - let firstByJsonPath<'T> tableName jsonPath = - WithProps.Find.firstByJsonPath<'T> tableName jsonPath (fromDataSource ()) + /// Retrieve documents matching a JSON containment query (@>) + let ByContains<'TDoc>(tableName, criteria: obj) = + WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) + [] + let byJsonPath<'TDoc> tableName jsonPath = + WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) + let ByJsonPath<'TDoc>(tableName, jsonPath) = + WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] + let firstByField<'TDoc> tableName fieldName op (value: obj) = + WithProps.Find.firstByField<'TDoc> tableName fieldName op value (fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj) = + WithProps.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found + [] + let firstByContains<'TDoc> tableName (criteria: obj) = + WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>); returns null if not found + let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = + WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found + [] + let firstByJsonPath<'TDoc> tableName jsonPath = + WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found + let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = + WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) /// Commands to update documents @@ -508,23 +722,37 @@ module Find = module Update = /// Update a full document - let full<'T> tableName docId (document: 'T) = - WithProps.Update.full<'T> tableName docId document (fromDataSource ()) + [] + let full tableName (docId: 'TKey) (document: 'TDoc) = + WithProps.Update.full tableName docId document (fromDataSource ()) /// Update a full document - let fullFunc<'T> tableName idFunc (document: 'T) = - WithProps.Update.fullFunc<'T> tableName idFunc document (fromDataSource ()) + [] + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithProps.Update.fullFunc tableName idFunc document (fromDataSource ()) + + /// Update a full document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + WithProps.Update.FullFunc(tableName, idFunc, document, fromDataSource ()) /// Update a partial document - let partialById tableName docId (partial: obj) = + [] + let partialById tableName (docId: 'TKey) (partial: 'TPartial) = WithProps.Update.partialById tableName docId partial (fromDataSource ()) + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) = + WithProps.Update.partialByField tableName fieldName op value partial (fromDataSource ()) + /// Update partial documents using a JSON containment query in the WHERE clause (@>) - let partialByContains tableName (criteria: obj) (partial: obj) = + [] + let partialByContains tableName (criteria: 'TCriteria) (partial: 'TPartial) = WithProps.Update.partialByContains tableName criteria partial (fromDataSource ()) /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - let partialByJsonPath tableName jsonPath (partial: obj) = + [] + let partialByJsonPath tableName jsonPath (partial: 'TPartial) = WithProps.Update.partialByJsonPath tableName jsonPath partial (fromDataSource ()) @@ -533,34 +761,21 @@ module Update = module Delete = /// Delete a document by its ID - let byId tableName docId = + [] + let byId tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (fromDataSource ()) - /// Delete documents by matching a JSON contains query (@>) - let byContains tableName (criteria: obj) = + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Delete.byField tableName fieldName op value (fromDataSource ()) + + /// Delete documents by matching a JSON containment query (@>) + [] + let byContains tableName (criteria: 'TContains) = WithProps.Delete.byContains tableName criteria (fromDataSource ()) /// Delete documents by matching a JSON Path match query (@?) + [] let byJsonPath tableName path = WithProps.Delete.byJsonPath tableName path (fromDataSource ()) - - -/// Commands to execute custom SQL queries -[] -module Custom = - - /// Execute a query that returns one or no results - let single<'T> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.single query parameters mapFunc (fromDataSource ()) - - /// Execute a query that returns a list of results - let list<'T> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.list query parameters mapFunc (fromDataSource ()) - - /// Execute a query that returns no results - let nonQuery query parameters = - WithProps.Custom.nonQuery query parameters (fromDataSource ()) - - /// Execute a query that returns a scalar value - let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.scalar query parameters mapFunc (fromDataSource ()) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 7f372db..21e82f2 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -38,6 +38,21 @@ module Query = [] let ensureTable name = Query.Definition.ensureTableFor name "TEXT" + + /// Document update queries + module Update = + + /// Query to update a partial document by its ID + [] + let partialById tableName = + $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {Query.whereById "@id"}""" + + /// Query to update a partial document via a comparison on a JSON field + [] + let partialByField tableName fieldName op = + sprintf + "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" + tableName (Query.whereByField fieldName op "@field") /// Parameter handling helpers diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index a90cb8f..4d755a5 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -1,9 +1,10 @@ -namespace BitBadger.Documents.Tests.CSharp; - -using Documents; using Expecto.CSharp; using Expecto; +namespace BitBadger.Documents.Tests.CSharp; + +using static Runner; + /// /// A test serializer that returns known values /// @@ -21,13 +22,14 @@ public static class CommonCSharpTests /// /// Unit tests /// - [Tests] public static Test Unit = - Runner.TestList("Common.C# Unit", new[] + [Tests] + public static Test Unit = + TestList("Common.C# Unit", new[] { - Runner.TestSequenced( - Runner.TestList("Configuration", new[] + TestSequenced( + TestList("Configuration", new[] { - Runner.TestCase("UseSerializer succeeds", () => + TestCase("UseSerializer succeeds", () => { try { @@ -49,12 +51,12 @@ public static class CommonCSharpTests Configuration.UseSerializer(DocumentSerializer.Default); } }), - Runner.TestCase("Serializer returns configured serializer", () => + TestCase("Serializer returns configured serializer", () => { Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), "Serializer should have been the same"); }), - Runner.TestCase("UseIdField / IdField succeeds", () => + TestCase("UseIdField / IdField succeeds", () => { try { @@ -69,89 +71,89 @@ public static class CommonCSharpTests } }) })), - Runner.TestList("Op", new[] + TestList("Op", new[] { - Runner.TestCase("EQ succeeds", () => + TestCase("EQ succeeds", () => { Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); }), - Runner.TestCase("GT succeeds", () => + TestCase("GT succeeds", () => { Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); }), - Runner.TestCase("GE succeeds", () => + TestCase("GE succeeds", () => { Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); }), - Runner.TestCase("LT succeeds", () => + TestCase("LT succeeds", () => { Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); }), - Runner.TestCase("LE succeeds", () => + TestCase("LE succeeds", () => { Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); }), - Runner.TestCase("NE succeeds", () => + TestCase("NE succeeds", () => { Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); }), - Runner.TestCase("EX succeeds", () => + TestCase("EX succeeds", () => { Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); }), - Runner.TestCase("NEX succeeds", () => + TestCase("NEX succeeds", () => { Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); }) }), - Runner.TestList("Query", new[] + TestList("Query", new[] { - Runner.TestCase("SelectFromTable succeeds", () => + TestCase("SelectFromTable succeeds", () => { Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", "SELECT statement not correct"); }), - Runner.TestCase("WhereById succeeds", () => + TestCase("WhereById succeeds", () => { Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); }), - Runner.TestList("WhereByField", new[] + TestList("WhereByField", new[] { - Runner.TestCase("succeeds when a logical operator is passed", () => + TestCase("succeeds when a logical operator is passed", () => { Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test", "WHERE clause not correct"); }), - Runner.TestCase("succeeds when an existence operator is passed", () => + TestCase("succeeds when an existence operator is passed", () => { Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL", "WHERE clause not correct"); }) }), - Runner.TestList("Definition", new[] + TestList("Definition", new[] { - Runner.TestCase("EnsureTableFor succeeds", () => + 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"); }), - Runner.TestList("EnsureKey", new[] + TestList("EnsureKey", new[] { - Runner.TestCase("succeeds when a schema is present", () => + 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"); }), - Runner.TestCase("succeeds when a schema is not present", () => + 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"); }) }), - Runner.TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => + TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", @@ -161,87 +163,72 @@ public static class CommonCSharpTests "CREATE INDEX for multiple field statement incorrect"); }) }), - Runner.TestCase("Insert succeeds", () => + TestCase("Insert succeeds", () => { Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); }), - Runner.TestCase("Save succeeds", () => + 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"); }), - Runner.TestList("Count", new[] + TestList("Count", new[] { - Runner.TestCase("All succeeds", () => + TestCase("All succeeds", () => { Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ), "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", "JSON field text comparison count query not correct"); }) }), - Runner.TestList("Exists", new[] + TestList("Exists", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Exists.ById("tbl"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it", "ID existence query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT), "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", "JSON field text comparison exists query not correct"); }) }), - Runner.TestList("Find", new[] + TestList("Find", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", "SELECT by ID query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE), "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", "SELECT by JSON comparison query not correct"); }) }), - Runner.TestList("Update", new[] + TestCase("Update.Full succeeds", () => { - Runner.TestCase("Full succeeds", () => - { - Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", - "UPDATE full statement not correct"); - }), - Runner.TestCase("PartialById succeeds", () => - { - Expect.equal(Query.Update.PartialById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - Runner.TestCase("PartialByField succeeds", () => - { - Expect.equal(Query.Update.PartialByField("tbl", "Part", Op.NE), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", - "UPDATE partial by JSON comparison query not correct"); - }) + Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", + "UPDATE full statement not correct"); }), - Runner.TestList("Delete", new[] + TestList("Delete", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", "DELETE by ID query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX), "DELETE FROM tbl WHERE data ->> 'gone' IS NULL", diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index f1c35c6..e314ec2 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -19,6 +19,29 @@ public static class SqliteCSharpTests public static Test Unit = TestList("Unit", new[] { + TestList("Query", new[] + { + 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"); + }), + TestList("Update", new[] + { + TestCase("PartialById succeeds", () => + { + Expect.equal(Sqlite.Query.Update.PartialById("tbl"), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); + }), + TestCase("PartialByField succeeds", () => + { + Expect.equal(Sqlite.Query.Update.PartialByField("tbl", "Part", Op.NE), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", + "UPDATE partial by JSON comparison query not correct"); + }) + }), + }), TestList("Parameters", new[] { TestCase("Id succeeds", () => diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 5dab352..a0ce0d7 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -141,18 +141,6 @@ let all = $"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id" "UPDATE full statement not correct" } - test "partialById succeeds" { - Expect.equal - (Query.Update.partialById tbl) - $"UPDATE {tbl} SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" - "UPDATE partial by ID statement not correct" - } - test "partialByField succeeds" { - Expect.equal - (Query.Update.partialByField tbl "Part" NE) - $"UPDATE {tbl} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" - "UPDATE partial by JSON comparison query not correct" - } ] testList "Delete" [ test "byId succeeds" { diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 0a7f558..afce064 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -1,6 +1,7 @@ module PostgresTests open Expecto +open BitBadger.Documents open BitBadger.Documents.Postgres open BitBadger.Documents.Tests @@ -30,13 +31,6 @@ let unitTests = } ] testList "Query" [ - test "selectFromTable succeeds" { - Expect.equal (Query.selectFromTable PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName}" - "SELECT statement not correct" - } - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct" - } test "whereDataContains succeeds" { Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" } @@ -65,10 +59,6 @@ let unitTests = "INSERT ON CONFLICT UPDATE statement 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 "byContains succeeds" { Expect.equal (Query.Count.byContains PostgresDb.TableName) $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -81,11 +71,6 @@ let unitTests = } ] 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 "byContains succeeds" { Expect.equal (Query.Exists.byContains PostgresDb.TableName) $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it" @@ -98,10 +83,6 @@ let unitTests = } ] 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 "byContains succeeds" { Expect.equal (Query.Find.byContains PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -114,11 +95,6 @@ let unitTests = } ] testList "Update" [ - test "full succeeds" { - Expect.equal (Query.Update.full PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = @data WHERE data ->> 'Id' = @id" - "UPDATE full statement not correct" - } test "partialById succeeds" { Expect.equal (Query.Update.partialById PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" @@ -136,10 +112,6 @@ let unitTests = } ] 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 "byContains succeeds" { Expect.equal (Query.Delete.byContains PostgresDb.TableName) $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -266,14 +238,14 @@ let integrationTests = let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } do! insert PostgresDb.TableName testDoc - let! before = Find.byId PostgresDb.TableName "test" - if Option.isNone before then Expect.isTrue false "There should have been a document returned" + 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" - if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" + 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" } ] @@ -378,7 +350,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.byId PostgresDb.TableName "two" + let! doc = Find.byId PostgresDb.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" } @@ -386,7 +358,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" + let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] @@ -481,9 +453,8 @@ let integrationTests = let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } do! Update.full PostgresDb.TableName "one" testDoc - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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" { @@ -504,9 +475,8 @@ let integrationTests = do! Update.fullFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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" } @@ -526,9 +496,8 @@ let integrationTests = do! loadDocs () do! Update.partialById PostgresDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + 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" { diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 3d185f5..2f16be6 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -6,6 +6,7 @@ let allTests = "BitBadger.Documents" [ CommonTests.all CommonCSharpTests.Unit + PostgresTests.all SqliteTests.all testSequenced SqliteExtensionTests.integrationTests SqliteCSharpTests.All diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index a632a35..819be57 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -10,6 +10,28 @@ open Types /// Unit tests for the SQLite library let unitTests = testList "Unit" [ + testList "Query" [ + 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" + } + testList "Update" [ + test "partialById succeeds" { + Expect.equal + (Query.Update.partialById "tbl") + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id" + "UPDATE partial by ID statement not correct" + } + test "partialByField succeeds" { + Expect.equal + (Query.Update.partialByField "tbl" "Part" NE) + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" + "UPDATE partial by JSON comparison query not correct" + } + ] + ] testList "Parameters" [ test "idParam succeeds" { let theParam = idParam 7 -- 2.45.1 From f3014acc2a7a790966ff8ee4cb091bbee8a02660 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 16:01:27 -0500 Subject: [PATCH 12/26] WIP on tests for new F# functions --- src/Postgres/Library.fs | 186 ++++++++----------- src/Tests.CSharp/PostgresDb.cs | 4 +- src/Tests/PostgresTests.fs | 316 +++++++++++++++++++-------------- 3 files changed, 263 insertions(+), 243 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 18ba007..4652ce6 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -1,7 +1,5 @@ namespace BitBadger.Documents.Postgres -open Npgsql - /// The type of index to generate for the document [] type DocumentIndex = @@ -11,11 +9,13 @@ type DocumentIndex = | Optimized +open Npgsql + /// Configuration for document handling module Configuration = /// The data source to use for query execution - let mutable private dataSourceValue : Npgsql.NpgsqlDataSource option = None + let mutable private dataSourceValue : NpgsqlDataSource option = None /// Register a data source to use for query execution (disposes the current one if it exists) let useDataSource source = @@ -38,10 +38,8 @@ module private Helpers = let internal fromDataSource () = Configuration.dataSource () |> Sql.fromDataSource - open System.Threading.Tasks - /// Execute a task and ignore the result - let internal ignoreTask<'T> (it : Task<'T>) = backgroundTask { + let internal ignoreTask<'T> (it : System.Threading.Tasks.Task<'T>) = backgroundTask { let! _ = it () } @@ -49,50 +47,36 @@ module private Helpers = open BitBadger.Documents -/// Data definition -[] -module Definition = - - /// SQL statement to create a document table - let createTable name = - $"CREATE TABLE IF NOT EXISTS %s{name} (data JSONB NOT NULL)" +/// Functions for creating parameters +[] +module Parameters = - /// SQL statement to create a key index for a document table - let createKey (name : string) = - let tableName = name.Split(".") |> Array.last - $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{tableName}_key ON {name} ((data ->> '{Configuration.idField ()}'))" - - /// SQL statement to create an index on documents in the specified table - let createIndex (name : string) idxType = - let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" - let tableName = name.Split(".") |> Array.last - $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" - - /// Definitions that take SqlProps as their last parameter - module WithProps = - - /// Create a document table - let ensureTable name sqlProps = backgroundTask { - do! sqlProps |> Sql.query (createTable name) |> Sql.executeNonQueryAsync |> ignoreTask - do! sqlProps |> Sql.query (createKey name) |> Sql.executeNonQueryAsync |> ignoreTask - } + /// Create an ID parameter (name "@id", key will be treated as a string) + [] + let idParam (key: 'TKey) = + "@id", Sql.string (string key) - /// Create an index on documents in the specified table - let ensureIndex name idxType sqlProps = - sqlProps |> Sql.query (createIndex name idxType) |> Sql.executeNonQueryAsync |> ignoreTask - - /// Create a document table - let ensureTable name = - WithProps.ensureTable name (fromDataSource ()) - - let ensureIndex name idxType = - WithProps.ensureIndex name idxType (fromDataSource ()) + /// 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 (name "@field") + [] + let fieldParam (value: obj) = + "@field", Sql.parameter (NpgsqlParameter("@field", value)) + /// An empty parameter sequence + [] + let noParams = + Seq.empty + + /// Query construction functions [] module Query = + /// Table and index definition queries module Definition = /// SQL statement to create a document table @@ -102,10 +86,10 @@ module Query = /// SQL statement to create an index on JSON documents in the specified table [] - let ensureJsonIndex (name : string) idxType = + let ensureJsonIndex (name: string) idxType = let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" let tableName = name.Split '.' |> Array.last - $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" + $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" /// Create a WHERE clause fragment to implement a @> (JSON contains) condition let whereDataContains paramName = @@ -115,23 +99,6 @@ module Query = let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" - /// Create a JSONB document parameter - let jsonbDocParam (it: obj) = - Sql.jsonb (Configuration.serializer().Serialize it) - - /// Create ID and data parameters for a query - let docParameters<'T> docId (doc: 'T) = - [ "@id", Sql.string docId; "@data", jsonbDocParam doc ] - - /// Query to insert a document - let insert tableName = - $"INSERT INTO %s{tableName} VALUES (@data)" - - /// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - let save tableName = - sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" - tableName (Configuration.idField ()) - /// Queries for counting documents module Count = @@ -196,31 +163,6 @@ module Query = $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" -/// Functions for creating parameters -[] -module Parameters = - - /// Create an ID parameter (name "@id", key will be treated as a string) - [] - let idParam (key: 'TKey) = - "@id", Sql.string (string key) - - /// 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 (name "@field") - [] - let fieldParam (value: obj) = - "@field", Sql.parameter (NpgsqlParameter("@field", value)) - - /// An empty parameter sequence - [] - let noParams = - Seq.empty - - /// Functions for dealing with results [] module Results = @@ -251,28 +193,28 @@ module WithProps = /// Execute a query that returns a list of results [] - let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = + let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps = Sql.query query sqlProps |> Sql.parameters parameters |> Sql.executeAsync mapFunc /// Execute a query that returns a list of results - let List<'T>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! results = list query (List.ofSeq parameters) mapFunc.Invoke sqlProps + let List<'TDoc>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { + let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps return ResizeArray results } - /// Execute a query that returns one or no results (returns None if not found) + /// Execute a query that returns one or no results; returns None if not found [] - let single<'T> query parameters mapFunc sqlProps = backgroundTask { - let! results = list<'T> query parameters mapFunc sqlProps + let single<'TDoc> query parameters mapFunc sqlProps = backgroundTask { + let! results = list<'TDoc> query parameters mapFunc sqlProps return FSharp.Collections.List.tryHead results } - /// Execute a query that returns one or no results (returns null if not found) - let Single<'T when 'T: null>( - query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { - let! result = single<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + /// 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 return Option.toObj result } @@ -295,21 +237,25 @@ module WithProps = let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func, sqlProps) = scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps - /// Execute a non-query statement to manipulate a document - let private executeNonQuery query (document: 'T) sqlProps = - sqlProps - |> Sql.query query - |> Sql.parameters [ "@data", Query.jsonbDocParam document ] - |> Sql.executeNonQueryAsync - |> ignoreTask + /// Table and index definition commands + module Definition = + + /// Create a document table + [] + let ensureTable name sqlProps = backgroundTask { + do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps + do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps + } - /// Execute a non-query statement to manipulate a document with an ID specified - let private executeNonQueryWithId query docId (document: 'T) sqlProps = - sqlProps - |> Sql.query query - |> Sql.parameters (Query.docParameters docId document) - |> Sql.executeNonQueryAsync - |> ignoreTask + /// Create an index on documents in the specified table + [] + let ensureJsonIndex name idxType sqlProps = + Custom.nonQuery (Query.Definition.ensureJsonIndex name idxType) [] sqlProps + + /// 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 /// Commands to add documents [] @@ -575,6 +521,26 @@ module Custom = WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ()) +/// Table and index definition commands +[] +module Definition = + + /// Create a document table + [] + let ensureTable name = + WithProps.Definition.ensureTable name (fromDataSource ()) + + /// Create an index on documents in the specified table + [] + let ensureJsonIndex name idxType = + WithProps.Definition.ensureJsonIndex name idxType (fromDataSource ()) + + /// Create an index on field(s) within documents in the specified table + [] + let ensureFieldIndex tableName indexName fields = + WithProps.Definition.ensureFieldIndex tableName indexName fields (fromDataSource ()) + + /// Document writing functions [] module Document = diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs index ce2ec6a..c8d2411 100644 --- a/src/Tests.CSharp/PostgresDb.cs +++ b/src/Tests.CSharp/PostgresDb.cs @@ -134,8 +134,8 @@ public static class PostgresDb var sqlProps = Sql.connect(database.ConnectionString); - Sql.executeNonQuery(Sql.query(Definition.createTable(TableName), sqlProps)); - Sql.executeNonQuery(Sql.query(Definition.createKey(TableName), sqlProps)); + Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps)); + Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps)); Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString)); diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index afce064..5d68908 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -8,105 +8,121 @@ open BitBadger.Documents.Tests /// Tests which do not hit the database let unitTests = testList "Unit" [ - testList "Definition" [ - test "createTable succeeds" { - Expect.equal (Definition.createTable PostgresDb.TableName) - $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" - "CREATE TABLE statement not constructed correctly" + testList "Parameters" [ + test "idParam succeeds" { + Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly" } - test "createKey succeeds" { - Expect.equal (Definition.createKey PostgresDb.TableName) - $"CREATE UNIQUE INDEX IF NOT EXISTS idx_{PostgresDb.TableName}_key ON {PostgresDb.TableName} ((data ->> 'Id'))" - "CREATE INDEX for key statement not constructed correctly" + test "jsonParam succeeds" { + Expect.equal + (jsonParam "@test" {| Something = "good" |}) + ("@test", Sql.jsonb """{"Something":"good"}""") + "JSON parameter not constructed correctly" } - test "createIndex succeeds for full index" { - Expect.equal (Definition.createIndex "schema.tbl" Full) - "CREATE INDEX IF NOT EXISTS idx_tbl ON schema.tbl USING GIN (data)" - "CREATE INDEX statement not constructed correctly" + test "fieldParam succeeds" { + let it = fieldParam 242 + 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 "createIndex succeeds for JSONB Path Ops index" { - Expect.equal (Definition.createIndex PostgresDb.TableName Optimized) - $"CREATE INDEX IF NOT EXISTS idx_{PostgresDb.TableName} ON {PostgresDb.TableName} USING GIN (data jsonb_path_ops)" - "CREATE INDEX statement not constructed correctly" + test "noParams succeeds" { + Expect.isEmpty noParams "The no-params sequence should be empty" } ] testList "Query" [ + 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 "ensureJsonIndex succeeds for full index" { + Expect.equal + (Query.Definition.ensureJsonIndex "schema.tbl" Full) + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)" + "CREATE INDEX statement not constructed correctly" + } + test "ensureJsonIndex succeeds for JSONB Path Ops index" { + Expect.equal + (Query.Definition.ensureJsonIndex 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 "jsonbDocParam succeeds" { - Expect.equal (Query.jsonbDocParam {| Hello = "There" |}) (Sql.jsonb "{\"Hello\":\"There\"}") - "JSONB document not serialized correctly" - } - test "docParameters succeeds" { - let parameters = Query.docParameters "abc123" {| Testing = 456 |} - let expected = [ - "@id", Sql.string "abc123" - "@data", Sql.jsonb "{\"Testing\":456}" - ] - Expect.equal parameters expected "There should have been 2 parameters, one string and one JSONB" - } - test "insert succeeds" { - Expect.equal (Query.insert PostgresDb.TableName) $"INSERT INTO {PostgresDb.TableName} VALUES (@data)" - "INSERT statement not correct" - } - test "save succeeds" { - Expect.equal (Query.save PostgresDb.TableName) - $"INSERT INTO {PostgresDb.TableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" - "INSERT ON CONFLICT UPDATE statement not correct" - } testList "Count" [ test "byContains succeeds" { - Expect.equal (Query.Count.byContains PostgresDb.TableName) + 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) + 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 "byContains succeeds" { - Expect.equal (Query.Exists.byContains PostgresDb.TableName) + 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) + 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 "byContains succeeds" { - Expect.equal (Query.Find.byContains PostgresDb.TableName) + 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) + 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 "Update" [ test "partialById succeeds" { - Expect.equal (Query.Update.partialById PostgresDb.TableName) + Expect.equal + (Query.Update.partialById PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" "UPDATE partial by ID statement not correct" } + test "partialByField succeeds" { + Expect.equal + (Query.Update.partialByField PostgresDb.TableName "Snail" LT) + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field" + "UPDATE partial by ID statement not correct" + } test "partialByContains succeeds" { - Expect.equal (Query.Update.partialByContains PostgresDb.TableName) + Expect.equal + (Query.Update.partialByContains PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" "UPDATE partial by JSON containment statement not correct" } test "partialByJsonPath succeeds" { - Expect.equal (Query.Update.partialByJsonPath PostgresDb.TableName) + Expect.equal + (Query.Update.partialByJsonPath PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" "UPDATE partial by JSON Path statement not correct" } @@ -126,7 +142,6 @@ let unitTests = ] ] -open Npgsql.FSharp open ThrowawayDb.Postgres open Types @@ -163,17 +178,82 @@ let integrationTests = "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 () = - Sql.connect db.ConnectionString - |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" - |> Sql.executeRowAsync (fun row -> row.bool "it") + Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists let keyExists () = - Sql.connect db.ConnectionString - |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" - |> Sql.executeRowAsync (fun row -> row.bool "it") + Custom.scalar + "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists let! exists = tableExists () let! alsoExists = keyExists () @@ -186,22 +266,38 @@ let integrationTests = Expect.isTrue exists' "The table should now exist" Expect.isTrue alsoExists' "The key index should now exist" } - testTask "ensureIndex succeeds" { + testTask "ensureJsonIndex succeeds" { use db = PostgresDb.BuildDb() let indexExists () = - Sql.connect db.ConnectionString - |> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured') AS it" - |> Sql.executeRowAsync (fun row -> row.bool "it") + 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.ensureIndex "ensured" Optimized + do! Definition.ensureTable "ensured" + do! Definition.ensureJsonIndex "ensured" Optimized let! exists' = indexExists () Expect.isTrue exists' "The index should now exist" // TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence } + 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" + // TODO: check for field definition + } ] testList "insert" [ testTask "succeeds" { @@ -217,8 +313,11 @@ let integrationTests = 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) + 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" } ] @@ -257,6 +356,13 @@ let integrationTests = let! theCount = Count.all PostgresDb.TableName Expect.equal theCount 5 "There should have been 5 matching documents" } + testTask "byField succeeds" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! theCount = Count.byField PostgresDb.TableName "Value" EQ "purple" + Expect.equal theCount 2 "There should have been 2 matching documents" + } testTask "byContains succeeds" { use db = PostgresDb.BuildDb() do! loadDocs () @@ -289,6 +395,22 @@ let integrationTests = 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 "Sub" EX "" + 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 "NumValue" EQ "six" + Expect.isFalse exists "There should not have been existing documents" + } + ] testList "byContains" [ testTask "succeeds when documents exist" { use db = PostgresDb.BuildDb() @@ -605,76 +727,8 @@ let integrationTests = } ] ] - testList "Custom" [ - 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 "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 "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" - } - ] ] |> testSequenced -let all = testList "FSharp.Documents" [ unitTests; integrationTests ] +let all = testList "Postgres" [ unitTests; integrationTests ] -- 2.45.1 From a8b2927b2cdcc44985ff84334d45bc08130633d2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 22:22:28 -0500 Subject: [PATCH 13/26] Finish Postgres F# tests --- src/Tests/PostgresTests.fs | 131 ++++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 25 deletions(-) diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 5d68908..b39e33c 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -281,7 +281,6 @@ let integrationTests = do! Definition.ensureJsonIndex "ensured" Optimized let! exists' = indexExists () Expect.isTrue exists' "The index should now exist" - // TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence } testTask "ensureFieldIndex succeeds" { use db = PostgresDb.BuildDb() @@ -296,7 +295,6 @@ let integrationTests = do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] let! exists' = indexExists () Expect.isTrue exists' "The index should now exist" - // TODO: check for field definition } ] testList "insert" [ @@ -473,7 +471,7 @@ let integrationTests = do! loadDocs () let! doc = Find.byId PostgresDb.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" { @@ -481,7 +479,23 @@ let integrationTests = do! loadDocs () let! doc = Find.byId PostgresDb.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 "byField" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + let! docs = Find.byField PostgresDb.TableName "Value" EQ "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 "Value" EQ "mauve" + Expect.isEmpty docs "There should have been no documents returned" } ] testList "byContains" [ @@ -497,7 +511,7 @@ let integrationTests = do! loadDocs () let! docs = Find.byContains PostgresDb.TableName {| Value = "mauve" |} - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + Expect.isEmpty docs "There should have been no documents returned" } ] testList "byJsonPath" [ @@ -513,7 +527,32 @@ let integrationTests = do! loadDocs () let! docs = Find.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" - Expect.isTrue (List.isEmpty docs) "There should have been no documents returned" + 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 "Value" EQ "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 "Value" EQ "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 "Value" EQ "absent" + Expect.isNone doc "There should not have been a document returned" } ] testList "firstByContains" [ @@ -522,7 +561,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByContains PostgresDb.TableName {| Value = "another" |} - 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 multiple documents are found" { @@ -530,7 +569,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByContains PostgresDb.TableName {| Sub = {| Foo = "green" |} |} - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + 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" { @@ -538,7 +577,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByContains PostgresDb.TableName {| Value = "absent" |} - 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 "firstByJsonPath" [ @@ -547,7 +586,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Value ? (@ == "FIRST!")""" - 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 "one" "The incorrect document was returned" } testTask "succeeds when multiple documents are found" { @@ -555,7 +594,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" - Expect.isTrue (Option.isSome doc) "There should have been a document returned" + 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" { @@ -563,7 +602,7 @@ let integrationTests = do! loadDocs () let! doc = Find.firstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" - Expect.isFalse (Option.isSome doc) "There should not have been a document returned" + Expect.isNone doc "There should not have been a document returned" } ] ] @@ -582,11 +621,13 @@ let integrationTests = testTask "succeeds when no document is updated" { use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + 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.full PostgresDb.TableName "test" + do! Update.full + PostgresDb.TableName + "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] @@ -599,17 +640,20 @@ let integrationTests = { 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 } + 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 = Find.all PostgresDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + 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.fullFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + do! Update.fullFunc + PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] testList "partialById" [ @@ -625,13 +669,32 @@ let integrationTests = testTask "succeeds when no document is updated" { use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + 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.partialById PostgresDb.TableName "test" {| Foo = "green" |} } ] + testList "partialByField" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + do! loadDocs () + + do! Update.partialByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = Count.byField PostgresDb.TableName "NumValue" EQ "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! Update.partialByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + } + ] testList "partialByContains" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() @@ -644,8 +707,8 @@ let integrationTests = testTask "succeeds when no document is updated" { use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + 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.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} @@ -663,8 +726,8 @@ let integrationTests = testTask "succeeds when no document is updated" { use db = PostgresDb.BuildDb() - let! before = Find.all PostgresDb.TableName - Expect.hasCountOf before 0u isTrue "There should have been no documents returned" + 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.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} @@ -690,6 +753,24 @@ let integrationTests = 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 "Value" EQ "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 "Value" EQ "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() -- 2.45.1 From a1559ad29ed12bb7721fa04607846bcb05db3754 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 22:54:09 -0500 Subject: [PATCH 14/26] Add Postgres C# unit tests --- src/Postgres/Library.fs | 18 +++ src/Tests.CSharp/PostgresCSharpTests.cs | 180 ++++++++++++++++++++++++ src/Tests.CSharp/SqliteCSharpTests.cs | 1 + src/Tests/Program.fs | 1 + 4 files changed, 200 insertions(+) create mode 100644 src/Tests.CSharp/PostgresCSharpTests.cs diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 4652ce6..e189f83 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -92,10 +92,12 @@ module Query = $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" /// Create a WHERE clause fragment to implement a @> (JSON contains) condition + [] let whereDataContains paramName = $"data @> %s{paramName}" /// Create a WHERE clause fragment to implement a @? (JSON Path match) condition + [] let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" @@ -103,10 +105,12 @@ module Query = module Count = /// 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"}""" @@ -114,10 +118,12 @@ module Query = module Exists = /// 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""" @@ -125,10 +131,12 @@ module Query = module Find = /// 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"}""" @@ -136,18 +144,22 @@ module Query = module Update = /// Query to update a document + [] let partialById tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}""" /// Query to update a document + [] let partialByField tableName fieldName op = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}""" /// Query to update partial documents matching a JSON containment query (@>) + [] let partialByContains tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}""" /// Query to update partial documents matching a JSON containment query (@>) + [] let partialByJsonPath tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}""" @@ -155,10 +167,12 @@ module Query = module Delete = /// 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"}""" @@ -168,18 +182,22 @@ module Query = module Results = /// Create a domain item from a document, specifying the field in which the document is found + [] let fromDocument<'T> field (row: RowReader) : 'T = Configuration.serializer().Deserialize<'T>(row.string field) /// Create a domain item from a document + [] let fromData<'T> row : 'T = fromDocument "data" row /// Extract a count from the column "it" + [] let toCount (row: RowReader) = row.int "it" /// Extract a true/false value from the column "it" + [] let toExists (row: RowReader) = row.bool "it" diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs new file mode 100644 index 0000000..34a623c --- /dev/null +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -0,0 +1,180 @@ +using Expecto.CSharp; +using Expecto; +using BitBadger.Documents.Postgres; +using Npgsql.FSharp; + +namespace BitBadger.Documents.Tests.CSharp; + +using static Runner; + +/// +/// C# tests for the PostgreSQL implementation of BitBadger.Documents +/// +public class PostgresCSharpTests +{ + public static Test Unit = + TestList("Unit", new[] + { + TestList("Parameters", new[] + { + TestCase("Id succeeds", () => { + Expect.equal(Parameters.Id(88).Item1, "@id", "ID parameter not constructed correctly"); + }), + TestCase("Json succeeds", () => + { + Expect.equal(Parameters.Json("@test", new { Something = "good" }).Item1, "@test", + "JSON parameter not constructed correctly"); + }), + TestCase("Field succeeds", () => + { + Expect.equal(Parameters.Field(242).Item1, "@field", "Field parameter not constructed correctly"); + }), + TestCase("None succeeds", () => { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + }) + }), + TestList("Query", new[] + { + TestList("Definition", new[] + { + 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("EnsureJsonIndex succeeds for full index", () => + { + Expect.equal(Postgres.Query.Definition.EnsureJsonIndex("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("EnsureJsonIndex succeeds for JSONB Path Ops index", () => + { + Expect.equal( + Postgres.Query.Definition.EnsureJsonIndex(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"); + }), + TestList("Count", new[] + { + 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("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("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("Update", new[] + { + TestCase("partialById succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialById(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); + }), + TestCase("partialByField succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByField(PostgresDb.TableName, "Snail", Op.LT), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", + "UPDATE partial by ID statement not correct"); + }), + TestCase("partialByContains succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByContains(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", + "UPDATE partial by JSON containment statement not correct"); + }), + TestCase("partialByJsonPath succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByJsonPath(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", + "UPDATE partial by JSON Path statement not correct"); + }) + }), + TestList("Delete", new[] + { + 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"); + }) + }) + }) + }); + + private static readonly List TestDocuments = new() + { + 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 } + }; + + internal static async Task LoadDocs() + { + foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); + } + + /// + /// All Postgres C# tests + /// + public static Test All = TestList("Postgres.C#", new[] { Unit }); +} diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index e314ec2..39ed2e9 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -16,6 +16,7 @@ public static class SqliteCSharpTests /// /// Unit tests for the SQLite library /// + [Tests] public static Test Unit = TestList("Unit", new[] { diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 2f16be6..30fdeef 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -7,6 +7,7 @@ let allTests = [ CommonTests.all CommonCSharpTests.Unit PostgresTests.all + PostgresCSharpTests.All SqliteTests.all testSequenced SqliteExtensionTests.integrationTests SqliteCSharpTests.All -- 2.45.1 From 18ec31d16bdf393df6618aeb6a525f837948b8b6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 11:25:36 -0500 Subject: [PATCH 15/26] Finish Postgres C# tests - Rename *JsonIndex to *DocumentIndex --- src/Postgres/Library.fs | 18 +- src/Tests.CSharp/CommonCSharpTests.cs | 366 +++--- src/Tests.CSharp/PostgresCSharpTests.cs | 1035 +++++++++++++++-- src/Tests.CSharp/PostgresDb.cs | 2 +- .../SqliteCSharpExtensionTests.cs | 890 +++++++------- src/Tests.CSharp/SqliteCSharpTests.cs | 967 ++++++++------- src/Tests/PostgresTests.fs | 16 +- 7 files changed, 2037 insertions(+), 1257 deletions(-) diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index e189f83..1ea570a 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -18,11 +18,13 @@ module Configuration = let mutable private dataSourceValue : NpgsqlDataSource option = None /// Register a data source to use for query execution (disposes the current one if it exists) + [] let useDataSource source = if Option.isSome dataSourceValue then dataSourceValue.Value.Dispose() dataSourceValue <- Some source /// Retrieve the currently configured data source + [] let dataSource () = match dataSourceValue with | Some source -> source @@ -85,8 +87,8 @@ module Query = Query.Definition.ensureTableFor name "JSONB" /// SQL statement to create an index on JSON documents in the specified table - [] - let ensureJsonIndex (name: string) idxType = + [] + let ensureDocumentIndex (name: string) idxType = let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" let tableName = name.Split '.' |> Array.last $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})" @@ -266,9 +268,9 @@ module WithProps = } /// Create an index on documents in the specified table - [] - let ensureJsonIndex name idxType sqlProps = - Custom.nonQuery (Query.Definition.ensureJsonIndex name idxType) [] sqlProps + [] + let ensureDocumentIndex name idxType sqlProps = + Custom.nonQuery (Query.Definition.ensureDocumentIndex name idxType) [] sqlProps /// Create an index on field(s) within documents in the specified table [] @@ -549,9 +551,9 @@ module Definition = WithProps.Definition.ensureTable name (fromDataSource ()) /// Create an index on documents in the specified table - [] - let ensureJsonIndex name idxType = - WithProps.Definition.ensureJsonIndex name idxType (fromDataSource ()) + [] + let ensureDocumentIndex name idxType = + WithProps.Definition.ensureDocumentIndex name idxType (fromDataSource ()) /// Create an index on field(s) within documents in the specified table [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 4d755a5..ecfb9f9 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -23,218 +23,216 @@ public static class CommonCSharpTests /// Unit tests /// [Tests] - public static Test Unit = - TestList("Common.C# Unit", new[] - { - TestSequenced( - TestList("Configuration", new[] - { - 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"); - } - }) - })), - TestList("Op", new[] + public static readonly Test Unit = TestList("Common.C# Unit", new[] + { + TestSequenced( + TestList("Configuration", new[] { - TestCase("EQ succeeds", () => + TestCase("UseSerializer succeeds", () => { - Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); + 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("GT succeeds", () => + TestCase("Serializer returns configured serializer", () => { - Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); + Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), + "Serializer should have been the same"); }), - TestCase("GE succeeds", () => + TestCase("UseIdField / IdField succeeds", () => { - Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); + 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"); + } + }) + })), + 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("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("Query", new[] + { + TestCase("SelectFromTable succeeds", () => + { + Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", + "SELECT statement not correct"); + }), + TestCase("WhereById succeeds", () => + { + Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); + }), + TestList("WhereByField", new[] + { + TestCase("succeeds when a logical operator is passed", () => + { + Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test", + "WHERE clause not correct"); }), - TestCase("LT succeeds", () => + TestCase("succeeds when an existence operator is passed", () => { - 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("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"); + Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL", + "WHERE clause not correct"); }) }), - TestList("Query", new[] + TestList("Definition", new[] { - TestCase("SelectFromTable succeeds", () => + TestCase("EnsureTableFor succeeds", () => { - Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", - "SELECT statement not correct"); + 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"); }), - TestCase("WhereById succeeds", () => + TestList("EnsureKey", new[] { - Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); - }), - TestList("WhereByField", new[] - { - TestCase("succeeds when a logical operator is passed", () => + TestCase("succeeds when a schema is present", () => { - Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test", - "WHERE clause not correct"); + 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 an existence operator is passed", () => + TestCase("succeeds when a schema is not present", () => { - Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL", - "WHERE clause not correct"); + Expect.equal(Query.Definition.EnsureKey("table"), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))", + "CREATE INDEX for key statement without schema not constructed correctly"); }) }), - TestList("Definition", new[] + TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => { - 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", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("test.table", "gibberish", - new[] { "taco", "guac DESC", "salsa ASC" }), - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", - "CREATE INDEX for multiple field statement incorrect"); - }) + Expect.equal( + Query.Definition.EnsureIndexOn("test.table", "gibberish", + new[] { "taco", "guac DESC", "salsa ASC" }), + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)", + "CREATE INDEX for multiple field statement incorrect"); + }) + }), + 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"); + }), + TestList("Count", new[] + { + TestCase("All succeeds", () => + { + Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); }), - TestCase("Insert succeeds", () => + TestCase("ByField succeeds", () => { - Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ), + "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", + "JSON field text comparison count query not correct"); + }) + }), + TestList("Exists", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Query.Exists.ById("tbl"), + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it", + "ID existence query not correct"); }), - TestCase("Save succeeds", () => + TestCase("ByField 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"); + Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT), + "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", + "JSON field text comparison exists query not correct"); + }) + }), + TestList("Find", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", + "SELECT by ID query not correct"); }), - TestList("Count", new[] + TestCase("ByField succeeds", () => { - TestCase("All succeeds", () => - { - Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", - "Count query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ), - "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", - "JSON field text comparison count query not correct"); - }) + Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE), + "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", + "SELECT by JSON comparison query not correct"); + }) + }), + TestCase("Update.Full succeeds", () => + { + Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", + "UPDATE full statement not correct"); + }), + TestList("Delete", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", + "DELETE by ID query not correct"); }), - TestList("Exists", new[] + TestCase("ByField succeeds", () => { - TestCase("ById succeeds", () => - { - Expect.equal(Query.Exists.ById("tbl"), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it", - "ID existence query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT), - "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", - "JSON field text comparison exists query not correct"); - }) - }), - TestList("Find", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", - "SELECT by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE), - "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", - "SELECT by JSON comparison query not correct"); - }) - }), - TestCase("Update.Full succeeds", () => - { - Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", - "UPDATE full statement not correct"); - }), - TestList("Delete", new[] - { - TestCase("ById succeeds", () => - { - Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", - "DELETE by ID query not correct"); - }), - TestCase("ByField succeeds", () => - { - Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX), - "DELETE FROM tbl WHERE data ->> 'gone' IS NULL", - "DELETE by JSON comparison query not correct"); - }) + Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX), + "DELETE FROM tbl WHERE data ->> 'gone' IS NULL", + "DELETE by JSON comparison query not correct"); }) }) - }); + }) + }); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 34a623c..72af8cb 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -1,10 +1,11 @@ using Expecto.CSharp; using Expecto; using BitBadger.Documents.Postgres; -using Npgsql.FSharp; +using ThrowawayDb.Postgres; namespace BitBadger.Documents.Tests.CSharp; +using static CommonExtensionsAndTypesForNpgsqlFSharp; using static Runner; /// @@ -12,152 +13,161 @@ using static Runner; /// public class PostgresCSharpTests { - public static Test Unit = - TestList("Unit", new[] + /// + /// Tests which do not hit the database + /// + private static readonly Test Unit = TestList("Unit", new[] + { + TestList("Parameters", new[] { - TestList("Parameters", new[] + TestCase("Id succeeds", () => { - TestCase("Id succeeds", () => { - Expect.equal(Parameters.Id(88).Item1, "@id", "ID parameter not constructed correctly"); - }), - TestCase("Json succeeds", () => + 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"); + }), + TestCase("Json succeeds", () => + { + 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"); + }), + TestCase("Field succeeds", () => + { + var it = Parameters.Field(242); + Expect.equal(it.Item1, "@field", "Field parameter not constructed correctly"); + Expect.isTrue(it.Item2.IsParameter, "Field parameter value incorrect"); + }), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + }) + }), + TestList("Query", new[] + { + TestList("Definition", new[] + { + TestCase("EnsureTable succeeds", () => { - Expect.equal(Parameters.Json("@test", new { Something = "good" }).Item1, "@test", - "JSON parameter not constructed correctly"); + 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("Field succeeds", () => + TestCase("EnsureDocumentIndex succeeds for full index", () => { - Expect.equal(Parameters.Field(242).Item1, "@field", "Field parameter not constructed correctly"); + 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("None succeeds", () => { - Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + 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"); }) }), - TestList("Query", new[] + TestCase("WhereDataContains succeeds", () => { - TestList("Definition", new[] + 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("ByContains succeeds", () => { - 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("EnsureJsonIndex succeeds for full index", () => - { - Expect.equal(Postgres.Query.Definition.EnsureJsonIndex("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("EnsureJsonIndex succeeds for JSONB Path Ops index", () => - { - Expect.equal( - Postgres.Query.Definition.EnsureJsonIndex(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"); - }) + 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("WhereDataContains succeeds", () => + TestCase("ByJsonPath succeeds", () => { - Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", - "WHERE clause not correct"); + 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("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("WhereJsonPathMatches succeeds", () => + TestCase("byJsonPath succeeds", () => { - Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", - "WHERE clause not correct"); + 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("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"); }), - TestList("Count", new[] + TestCase("byJsonPath succeeds", () => { - 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"); - }) + 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("Update", new[] + { + TestCase("partialById succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialById(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); }), - TestList("Exists", new[] + TestCase("partialByField succeeds", () => { - 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"); - }) + Expect.equal(Postgres.Query.Update.PartialByField(PostgresDb.TableName, "Snail", Op.LT), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", + "UPDATE partial by ID statement not correct"); }), - TestList("Find", new[] + TestCase("partialByContains succeeds", () => { - 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"); - }) + Expect.equal(Postgres.Query.Update.PartialByContains(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", + "UPDATE partial by JSON containment statement not correct"); }), - TestList("Update", new[] + TestCase("partialByJsonPath succeeds", () => { - TestCase("partialById succeeds", () => - { - Expect.equal(Postgres.Query.Update.PartialById(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - TestCase("partialByField succeeds", () => - { - Expect.equal(Postgres.Query.Update.PartialByField(PostgresDb.TableName, "Snail", Op.LT), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", - "UPDATE partial by ID statement not correct"); - }), - TestCase("partialByContains succeeds", () => - { - Expect.equal(Postgres.Query.Update.PartialByContains(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", - "UPDATE partial by JSON containment statement not correct"); - }), - TestCase("partialByJsonPath succeeds", () => - { - Expect.equal(Postgres.Query.Update.PartialByJsonPath(PostgresDb.TableName), - $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", - "UPDATE partial by JSON Path statement not correct"); - }) + Expect.equal(Postgres.Query.Update.PartialByJsonPath(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", + "UPDATE partial by JSON Path statement not correct"); + }) + }), + TestList("Delete", new[] + { + 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"); }), - TestList("Delete", new[] + TestCase("byJsonPath succeeds", () => { - 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"); - }) + Expect.equal(Postgres.Query.Delete.ByJsonPath(PostgresDb.TableName), + $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + "DELETE by JSON Path match query not correct"); }) }) - }); + }) + }); private static readonly List TestDocuments = new() { @@ -168,13 +178,794 @@ public class PostgresCSharpTests new() { Id = "five", Value = "purple", NumValue = 18 } }; + /// + /// Add the test documents to the database + /// internal static async Task LoadDocs() { foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } + /// + /// Integration tests for the PostgreSQL library + /// + private static readonly Test Integration = TestList("Integration", new[] + { + TestList("Configuration", new[] + { + 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 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"); + }) + }), + 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 () => + { + 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 + { + 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 + } + }) + }), + TestList("Save", new[] + { + 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"); + }) + }) + }), + TestList("Count", new[] + { + 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"); + }), + TestCase("ByField succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByField(PostgresDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2, "There should have been 2 matching documents"); + }), + 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[] + { + TestList("ById", new[] + { + 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("ByField", new[] + { + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByField(PostgresDb.TableName, "Sub", Op.NEX, ""); + 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, "NumValue", Op.EQ, "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"); + }) + }) + }), + TestList("Find", new[] + { + TestList("All", new[] + { + 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.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, "Value", Op.EQ, "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, "Value", Op.EQ, "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, "Value", Op.EQ, "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, "Value", Op.EQ, "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, "Value", Op.EQ, "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("Full", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.Full(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.Full(PostgresDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + TestList("FullFunc", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.FullFunc(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.FullFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("PartialById", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.PartialById(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 Update.PartialById(PostgresDb.TableName, "test", new { Foo = "green" }); + }) + }), + TestList("PartialByField", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.PartialByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + var after = await Count.ByField(PostgresDb.TableName, "NumValue", Op.EQ, "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 Update.PartialByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); + }) + }), + TestList("PartialByContains", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.PartialByContains(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 Update.PartialByContains(PostgresDb.TableName, new { Value = "burgundy" }, + new { Foo = "green" }); + }) + }), + TestList("PartialByJsonPath", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await LoadDocs(); + + await Update.PartialByJsonPath(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 Update.PartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + }) + }) + }), + 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, "Value", Op.EQ, "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, "Value", Op.EQ, "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"); + }) + }) + }) + }); + /// /// All Postgres C# tests /// - public static Test All = TestList("Postgres.C#", new[] { Unit }); + [Tests] + public static readonly Test All = TestList("Postgres.C#", new[] { Unit, TestSequenced(Integration) }); } diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs index c8d2411..2ee10e1 100644 --- a/src/Tests.CSharp/PostgresDb.cs +++ b/src/Tests.CSharp/PostgresDb.cs @@ -137,7 +137,7 @@ public static class PostgresDb Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps)); Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps)); - Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString)); + Postgres.Configuration.UseDataSource(MkDataSource(database.ConnectionString)); return new ThrowawayPostgresDb(database); } diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index cabe980..9015d26 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -13,499 +13,493 @@ using static Runner; public static class SqliteCSharpExtensionTests { private static Task LoadDocs() => SqliteCSharpTests.LoadDocs(); - + + /// + /// Integration tests for the SQLite extension methods + /// [Tests] - public static Test Integration = - TestList("Extensions", new[] + public static readonly Test Integration = TestList("Extensions", new[] + { + TestList("CustomSingle", new[] { - TestList("CustomSingle", new[] - { - TestCase("succeeds when a row is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.CustomSingle( - $"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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.CustomSingle( - $"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("CustomList", new[] - { - 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, - 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var docs = await conn.CustomList( - $"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("CustomNonQuery", new[] - { - TestCase("succeeds when operating on data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); - - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.CustomNonQuery( - $"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", - new[] { new SqliteParameter("@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(); - await using var conn = Sqlite.Configuration.DbConn(); - - var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); - Expect.equal(nbr, 5, "The query should have returned the number 5"); - }), - TestCase("EnsureTable succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - - Func> itExists = async name => - { - var result = await conn.CustomScalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); - return result > 0L; - }; - - 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"); - Expect.isTrue(exists, "The table should now exist"); - Expect.isTrue(alsoExists, "The key index should now exist"); - }), - TestList("Insert", new[] - { - TestCase("succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); - await conn.Insert(SqliteDb.TableName, - new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); - var after = await conn.FindAll(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); - }), - TestCase("fails for duplicate key", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - try - { - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); - } - catch (Exception) - { - // This is what is supposed to happen - } - }) - }), - TestList("Save", new[] - { - TestCase("succeeds when a document is inserted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); - - await conn.Save(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - var after = await conn.FindAll(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); - }), - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await conn.Insert(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - - var before = await conn.FindById(SqliteDb.TableName, "test"); - Expect.isNotNull(before, "There should have been a document returned"); - Expect.equal(before!.Id, "test", "The document is not correct"); - Expect.isNotNull(before.Sub, "There should have been a sub-document"); - Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); - Expect.equal(before.Sub.Bar, "b", "The document is not correct"); - - await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); - var after = await conn.FindById(SqliteDb.TableName, "test"); - Expect.isNotNull(after, "There should have been a document returned post-update"); - 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 () => + TestCase("succeeds when a row is found", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var theCount = await conn.CountAll(SqliteDb.TableName); - Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + var doc = await conn.CustomSingle($"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("CountByField succeeds", async () => + TestCase("succeeds when a row is not found", async () => { await using var db = await SqliteDb.BuildDb(); await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); - Expect.equal(theCount, 2L, "There should have been 2 matching documents"); - }), - TestList("ExistsById", new[] + var doc = await conn.CustomSingle($"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("CustomList", new[] + { + TestCase("succeeds when data is found", async () => { - TestCase("succeeds when a document exists", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - var exists = await conn.ExistsById(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); - Expect.isFalse(exists, "There should not have been an existing document"); - }) + var docs = await conn.CustomList(Query.SelectFromTable(SqliteDb.TableName), Parameters.None, + Results.FromData); + Expect.equal(docs.Count, 5, "There should have been 5 documents returned"); }), - TestList("ExistsByField", new[] + TestCase("succeeds when data is not found", async () => { - TestCase("succeeds when documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - var exists = await conn.ExistsByField(SqliteDb.TableName, "NumValue", Op.GE, 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var exists = await conn.ExistsByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) - }), - TestList("FindAll", new[] + var docs = await conn.CustomList( + $"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("CustomNonQuery", new[] + { + TestCase("succeeds when operating on data", async () => { - TestCase("succeeds when there is data", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); - await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName}", Parameters.None); - var results = await conn.FindAll(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var results = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(results, "There should have been no documents returned"); - }) + var remaining = await conn.CountAll(SqliteDb.TableName); + Expect.equal(remaining, 0L, "There should be no documents remaining in the table"); }), - TestList("FindById", new[] + TestCase("succeeds when no data matches where clause", async () => { - TestCase("succeeds when a document is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - var doc = await conn.FindById(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await conn.CustomNonQuery($"DELETE FROM {SqliteDb.TableName} WHERE data ->> 'NumValue' > @value", + new[] { new SqliteParameter("@value", 100) }); - var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); - Expect.isNull(doc, "There should not have been a document returned"); - }) - }), - TestList("FindByField", new[] + 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(); + await using var conn = Sqlite.Configuration.DbConn(); + + var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, rdr => rdr.GetInt32(0)); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }), + TestCase("EnsureTable succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + Func> itExists = async name => { - TestCase("succeeds when documents are found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + var result = await conn.CustomScalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); + return result > 0L; + }; - var docs = await conn.FindByField(SqliteDb.TableName, "NumValue", Op.GT, 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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + 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"); - var docs = await conn.FindByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); - Expect.isEmpty(docs, "There should have been no documents returned"); - }) - }), - TestList("FindFirstByField", new[] + await conn.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"); + }), + TestList("Insert", new[] + { + TestCase("succeeds", async () => { - 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, "Value", Op.EQ, - "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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.FindFirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, - "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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var doc = await conn.FindFirstByField(SqliteDb.TableName, "Value", Op.EQ, - "absent"); - Expect.isNull(doc, "There should not have been a document returned"); - }) + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); }), - TestList("UpdateFull", new[] + TestCase("fails for duplicate key", async () => { - TestCase("succeeds when a document is updated", async () => + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + try { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); - var after = await conn.FindById(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 Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); + } + catch (Exception) { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdateFull(SqliteDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) - }), - TestList("UpdateFullFunc", new[] + // This is what is supposed to happen + } + }) + }), + TestList("Save", new[] + { + TestCase("succeeds when a document is inserted", async () => { - 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 using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); - await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - var after = await conn.FindById(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) + await conn.Save(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await conn.FindAll(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); }), - TestList("UpdatePartialById", new[] + TestCase("succeeds when a document is updated", async () => { - 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 using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await conn.Insert(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - await conn.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); - var after = await conn.FindById(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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); + var before = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before!.Id, "test", "The document is not correct"); + Expect.isNotNull(before.Sub, "There should have been a sub-document"); + Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); + Expect.equal(before.Sub.Bar, "b", "The document is not correct"); - // This not raising an exception is the test - await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); - }) - }), - TestList("UpdatePartialByField", new[] + await conn.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + var after = await conn.FindById(SqliteDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + 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(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountAll(SqliteDb.TableName); + Expect.equal(theCount, 5L, "There should have been 5 matching documents"); + }), + TestCase("CountByField succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var theCount = await conn.CountByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + }), + TestList("ExistsById", new[] + { + TestCase("succeeds when a document exists", async () => { - 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 using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", - new { NumValue = 77 }); - var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 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(); - await using var conn = Sqlite.Configuration.DbConn(); - var before = await conn.FindAll(SqliteDb.TableName); - Expect.isEmpty(before, "There should have been no documents returned"); - - // This not raising an exception is the test - await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); - }) + var exists = await conn.ExistsById(SqliteDb.TableName, "three"); + Expect.isTrue(exists, "There should have been an existing document"); }), - TestList("DeleteById", new[] + TestCase("succeeds when a document does not exist", async () => { - TestCase("succeeds when a document is deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - await conn.DeleteById(SqliteDb.TableName, "four"); - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.DeleteById(SqliteDb.TableName, "thirty"); - var remaining = await conn.CountAll(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) - }), - TestList("DeleteByField", new[] + var exists = await conn.ExistsById(SqliteDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + }), + TestList("ExistsByField", new[] + { + TestCase("succeeds when documents exist", async () => { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); - await conn.DeleteByField(SqliteDb.TableName, "Value", Op.NE, "purple"); - var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); - await LoadDocs(); - - await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "crimson"); - var remaining = await conn.CountAll(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) + var exists = await conn.ExistsByField(SqliteDb.TableName, "NumValue", Op.GE, 10); + Expect.isTrue(exists, "There should have been existing documents"); }), - TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + TestCase("succeeds when no matching 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, "Nothing", Op.EQ, "none"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }), + TestList("FindAll", new[] + { + TestCase("succeeds when there is data", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "one", Value = "two" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "three", Value = "four" }); + await conn.Insert(SqliteDb.TableName, new JsonDocument { Id = "five", Value = "six" }); + + var results = await conn.FindAll(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var results = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + }), + TestList("FindById", new[] + { + 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.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindById(SqliteDb.TableName, "eighty-seven"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("FindByField", new[] + { + 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, "NumValue", Op.GT, 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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var docs = await conn.FindByField(SqliteDb.TableName, "Value", Op.EQ, "mauve"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindFirstByField", new[] + { + 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, "Value", Op.EQ, "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Sub.Foo", Op.EQ, "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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var doc = await conn.FindFirstByField(SqliteDb.TableName, "Value", Op.EQ, "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("UpdateFull", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); + var after = await conn.FindById(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 using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFull(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + TestList("UpdateFullFunc", new[] + { + 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.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("UpdatePartialById", new[] + { + 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.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + var after = await conn.FindById(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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByField", new[] + { + 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.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 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(); + await using var conn = Sqlite.Configuration.DbConn(); + var before = await conn.FindAll(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); + }) + }), + TestList("DeleteById", new[] + { + TestCase("succeeds when a document is deleted", async () => + { + await using var db = await SqliteDb.BuildDb(); + await using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "four"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteById(SqliteDb.TableName, "thirty"); + var remaining = await conn.CountAll(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByField", new[] + { + 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, "Value", Op.NE, "purple"); + var remaining = await conn.CountAll(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 using var conn = Sqlite.Configuration.DbConn(); + await LoadDocs(); + + await conn.DeleteByField(SqliteDb.TableName, "Value", Op.EQ, "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 39ed2e9..a511856 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -16,60 +16,58 @@ public static class SqliteCSharpTests /// /// Unit tests for the SQLite library /// - [Tests] - public static Test Unit = - TestList("Unit", new[] + private static readonly Test Unit = TestList("Unit", new[] + { + TestList("Query", new[] { - TestList("Query", new[] + TestCase("Definition.EnsureTable succeeds", () => { - 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"); - }), - TestList("Update", new[] - { - TestCase("PartialById succeeds", () => - { - Expect.equal(Sqlite.Query.Update.PartialById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - TestCase("PartialByField succeeds", () => - { - Expect.equal(Sqlite.Query.Update.PartialByField("tbl", "Part", Op.NE), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", - "UPDATE partial by JSON comparison query not correct"); - }) - }), + Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); }), - TestList("Parameters", new[] + TestList("Update", new[] { - TestCase("Id succeeds", () => + TestCase("PartialById 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"); + Expect.equal(Sqlite.Query.Update.PartialById("tbl"), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); }), - TestCase("Json succeeds", () => + TestCase("PartialByField 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("Field succeeds", () => - { - var theParam = Parameters.Field(99); - Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); - Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); - }), - TestCase("None succeeds", () => - { - Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); + Expect.equal(Sqlite.Query.Update.PartialByField("tbl", "Part", Op.NE), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", + "UPDATE partial by JSON comparison query not correct"); }) + }), + }), + TestList("Parameters", new[] + { + 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("Field succeeds", () => + { + var theParam = Parameters.Field(99); + Expect.equal(theParam.ParameterName, "@field", "The parameter name is incorrect"); + Expect.equal(theParam.Value, 99, "The parameter value is incorrect"); + }), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The parameter list should have been empty"); }) - // Results are exhaustively executed in the context of other tests - }); + }) + // Results are exhaustively executed in the context of other tests + }); private static readonly List TestDocuments = new() { @@ -80,507 +78,504 @@ public static class SqliteCSharpTests new() { Id = "five", Value = "purple", NumValue = 18 } }; + /// + /// Add the test documents to the database + /// internal static async Task LoadDocs() { foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); } - [Tests] - public static Test Integration = - TestList("Integration", new[] + private static readonly Test Integration = TestList("Integration", new[] + { + TestCase("Configuration.UseConnectionString succeeds", () => { - TestCase("Configuration.UseConnectionString succeeds", () => + 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:"); + } + }), + TestList("Custom", new[] + { + TestList("Single", new[] + { + TestCase("succeeds when a row is found", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + 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 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(); + + 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(); + + 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) + { + var result = await Custom.Scalar( + $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", + new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); + return result > 0L; + } + }) + }), + TestList("Document.Insert", new[] + { + TestCase("succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + await Document.Insert(SqliteDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await Find.All(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); 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:"); - } - }), - TestList("Custom", new[] - { - TestList("Single", new [] - { - TestCase("succeeds when a row is found", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - 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 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(); - - 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(); - - 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) - { - var result = await Custom.Scalar( - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, - rdr => rdr.GetInt64(0)); - return result > 0L; - } - }) - }), - TestList("Document.Insert", new[] - { - TestCase("succeeds", async () => - { - await using var db = await SqliteDb.BuildDb(); - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); - await Document.Insert(SqliteDb.TableName, - new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); - var after = await Find.All(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); - }), - TestCase("fails for duplicate key", async () => - { - await using var db = await SqliteDb.BuildDb(); await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - try - { - await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "test" }); - Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); - } - catch (Exception) - { - // This is what is supposed to happen - } - }) - }), - TestList("Document.Save", new[] + Expect.isTrue(false, "An exception should have been raised for duplicate document ID insert"); + } + catch (Exception) + { + // This is what is supposed to happen + } + }) + }), + TestList("Document.Save", new[] + { + TestCase("succeeds when a document is inserted", async () => { - TestCase("succeeds when a document is inserted", async () => + await using var db = await SqliteDb.BuildDb(); + var before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should be no documents in the table"); + + await Document.Save(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await Find.All(SqliteDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = await SqliteDb.BuildDb(); + await Document.Insert(SqliteDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await Find.ById(SqliteDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before!.Id, "test", "The document is not correct"); + Expect.isNotNull(before.Sub, "There should have been a sub-document"); + Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); + Expect.equal(before.Sub.Bar, "b", "The document is not correct"); + + await Document.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); + var after = await Find.ById(SqliteDb.TableName, "test"); + Expect.isNotNull(after, "There should have been a document returned post-update"); + 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[] + { + 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"); + }), + TestCase("ByField succeeds", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var theCount = await Count.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + }) + }), + TestList("Exists", new[] + { + TestList("ById", new[] + { + TestCase("succeeds when a document exists", async () => { await using var db = await SqliteDb.BuildDb(); - var before = await Find.All(SqliteDb.TableName); - Expect.isEmpty(before, "There should be no documents in the table"); + await LoadDocs(); - await Document.Save(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); - var after = await Find.All(SqliteDb.TableName); - Expect.equal(after.Count, 1, "There should have been one document inserted"); + 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"); + }) + }), + TestList("ByField", new[] + { + TestCase("succeeds when documents exist", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + var exists = await Exists.ByField(SqliteDb.TableName, "NumValue", Op.GE, 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, "Nothing", Op.EQ, "none"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }) + }), + TestList("Find", new[] + { + TestList("All", new[] + { + TestCase("succeeds when there is data", 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 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"); + }) + }), + TestList("ById", new[] + { + 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("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, "NumValue", Op.GT, 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, "Value", Op.EQ, "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, "Value", Op.EQ, "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, "Sub.Foo", Op.EQ, "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, "Value", Op.EQ, "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }) + }), + TestList("Update", new[] + { + TestList("Full", new[] + { TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); - await Document.Insert(SqliteDb.TableName, - new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + await LoadDocs(); - var before = await Find.ById(SqliteDb.TableName, "test"); - Expect.isNotNull(before, "There should have been a document returned"); - Expect.equal(before!.Id, "test", "The document is not correct"); - Expect.isNotNull(before.Sub, "There should have been a sub-document"); - Expect.equal(before.Sub!.Foo, "a", "The document is not correct"); - Expect.equal(before.Sub.Bar, "b", "The document is not correct"); - - await Document.Save(SqliteDb.TableName, new JsonDocument { Id = "test" }); - var after = await Find.ById(SqliteDb.TableName, "test"); + var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; + await Update.Full(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, "test", "The updated document is not correct"); - Expect.isNull(after.Sub, "There should not have been a sub-document in the updated document"); + 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.Full(SqliteDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - TestList("Count", new[] + TestList("FullFunc", new[] { - TestCase("All succeeds", async () => + TestCase("succeeds when a document is updated", 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"); + await Update.FullFunc(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("ByField succeeds", async () => + 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.FullFunc(SqliteDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("PartialById", new[] + { + TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - var theCount = await Count.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple"); - Expect.equal(theCount, 2L, "There should have been 2 matching documents"); + await Update.PartialById(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 Update.PartialById(SqliteDb.TableName, "test", new { Foo = "green" }); }) }), - TestList("Exists", new[] + TestList("PartialByField", new[] { - TestList("ById", new[] + TestCase("succeeds when a document is updated", 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"); - }) + await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", + new { NumValue = 77 }); + var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 77); + Expect.equal(after, 2L, "There should have been 2 documents returned"); }), - TestList("ByField", new[] + TestCase("succeeds when no document is updated", async () => { - TestCase("succeeds when documents exist", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); + await using var db = await SqliteDb.BuildDb(); - var exists = await Exists.ByField(SqliteDb.TableName, "NumValue", Op.GE, 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 before = await Find.All(SqliteDb.TableName); + Expect.isEmpty(before, "There should have been no documents returned"); - var exists = await Exists.ByField(SqliteDb.TableName, "Nothing", Op.EQ, "none"); - Expect.isFalse(exists, "There should not have been any existing documents"); - }) + // This not raising an exception is the test + await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); }) - }), - TestList("Find", new[] + }) + }), + TestList("Delete", new[] + { + TestList("ById", new[] { - TestList("All", new[] + TestCase("succeeds when a document is deleted", 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"); - }) + await Delete.ById(SqliteDb.TableName, "four"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 4L, "There should have been 4 documents remaining"); }), - TestList("ById", new[] + TestCase("succeeds when a document is not deleted", 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, "NumValue", Op.GT, 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, "Value", Op.EQ, "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, "Value", Op.EQ, "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, "Sub.Foo", Op.EQ, "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, "Value", Op.EQ, "absent"); - Expect.isNull(doc, "There should not have been a document returned"); - }) + 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("Update", new[] + TestList("ByField", new[] { - TestList("Full", new[] + TestCase("succeeds when documents are deleted", 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(); + await LoadDocs(); - var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await Update.Full(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.Full(SqliteDb.TableName, "test", - new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); - }) + await Delete.ByField(SqliteDb.TableName, "Value", Op.NE, "purple"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 2L, "There should have been 2 documents remaining"); }), - TestList("FullFunc", new[] + TestCase("succeeds when documents are not deleted", 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(); + await LoadDocs(); - await Update.FullFunc(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.FullFunc(SqliteDb.TableName, doc => doc.Id, - new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); - }) - }), - TestList("PartialById", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Update.PartialById(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 Update.PartialById(SqliteDb.TableName, "test", new { Foo = "green" }); - }) - }), - TestList("PartialByField", new[] - { - TestCase("succeeds when a document is updated", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", - new { NumValue = 77 }); - var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 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 Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); - }) + await Delete.ByField(SqliteDb.TableName, "Value", Op.EQ, "crimson"); + var remaining = await Count.All(SqliteDb.TableName); + Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); }) - }), - 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"); - }) - }), - TestList("ByField", new[] - { - TestCase("succeeds when documents are deleted", async () => - { - await using var db = await SqliteDb.BuildDb(); - await LoadDocs(); - - await Delete.ByField(SqliteDb.TableName, "Value", Op.NE, "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, "Value", Op.EQ, "crimson"); - var remaining = await Count.All(SqliteDb.TableName); - Expect.equal(remaining, 5L, "There should have been 5 documents remaining"); - }) - }) - }), - TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) - }); + }) + }), + TestCase("Clean up database", () => Sqlite.Configuration.UseConnectionString("data source=:memory:")) + }); /// /// All tests for SQLite C# functions and methods /// + [Tests] public static readonly Test All = TestList("Sqlite.C#", new[] { Unit, TestSequenced(Integration) }); } diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index b39e33c..46fbce0 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -39,15 +39,15 @@ let unitTests = $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)" "CREATE TABLE statement not constructed correctly" } - test "ensureJsonIndex succeeds for full index" { + test "ensureDocumentIndex succeeds for full index" { Expect.equal - (Query.Definition.ensureJsonIndex "schema.tbl" Full) + (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 "ensureJsonIndex succeeds for JSONB Path Ops index" { + test "ensureDocumentIndex succeeds for JSONB Path Ops index" { Expect.equal - (Query.Definition.ensureJsonIndex PostgresDb.TableName Optimized) + (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" @@ -266,7 +266,7 @@ let integrationTests = Expect.isTrue exists' "The table should now exist" Expect.isTrue alsoExists' "The key index should now exist" } - testTask "ensureJsonIndex succeeds" { + testTask "ensureDocumentIndex succeeds" { use db = PostgresDb.BuildDb() let indexExists () = Custom.scalar @@ -277,8 +277,8 @@ let integrationTests = let! exists = indexExists () Expect.isFalse exists "The index should not exist already" - do! Definition.ensureTable "ensured" - do! Definition.ensureJsonIndex "ensured" Optimized + do! Definition.ensureTable "ensured" + do! Definition.ensureDocumentIndex "ensured" Optimized let! exists' = indexExists () Expect.isTrue exists' "The index should now exist" } @@ -730,7 +730,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + do! Update.partialByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} } ] ] -- 2.45.1 From fd7c9a7a25688687daa3e9f2113390a707d33d44 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 12:25:55 -0500 Subject: [PATCH 16/26] Move Sqlite extensions to Sqlite project --- src/BitBadger.Documents.sln | 6 ------ .../BitBadger.Documents.Sqlite.Extensions.fsproj | 11 ----------- src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 1 + .../Library.fs => Sqlite/Extensions.fs} | 2 +- .../BitBadger.Documents.Tests.CSharp.csproj | 1 - src/Tests/BitBadger.Documents.Tests.fsproj | 1 - 6 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj rename src/{Sqlite.Extensions/Library.fs => Sqlite/Extensions.fs} (99%) diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln index 9381b10..4012c67 100644 --- a/src/BitBadger.Documents.sln +++ b/src/BitBadger.Documents.sln @@ -13,8 +13,6 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitBadger.Documents.Tests.CSharp", "Tests.CSharp\BitBadger.Documents.Tests.CSharp.csproj", "{AB58418C-7F90-467E-8F67-F4E0AD9D8875}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite.Extensions", "Sqlite.Extensions\BitBadger.Documents.Sqlite.Extensions.fsproj", "{D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}" -EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Postgres", "Postgres\BitBadger.Documents.Postgres.fsproj", "{30E73486-9D00-440B-B4AC-5B7AC029AE72}" EndProject Global @@ -46,10 +44,6 @@ Global {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB58418C-7F90-467E-8F67-F4E0AD9D8875}.Release|Any CPU.Build.0 = Release|Any CPU - {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D416A5C8-B746-4FDF-8EC9-9CA0B8DA1384}.Release|Any CPU.Build.0 = Release|Any CPU {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Debug|Any CPU.Build.0 = Debug|Any CPU {30E73486-9D00-440B-B4AC-5B7AC029AE72}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj b/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj deleted file mode 100644 index e01f9dc..0000000 --- a/src/Sqlite.Extensions/BitBadger.Documents.Sqlite.Extensions.fsproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index 9a59aed..103f75b 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -2,6 +2,7 @@ + diff --git a/src/Sqlite.Extensions/Library.fs b/src/Sqlite/Extensions.fs similarity index 99% rename from src/Sqlite.Extensions/Library.fs rename to src/Sqlite/Extensions.fs index 011746e..bacd13b 100644 --- a/src/Sqlite.Extensions/Library.fs +++ b/src/Sqlite/Extensions.fs @@ -1,4 +1,4 @@ -namespace BitBadger.Documents.Sqlite +namespace BitBadger.Documents.Sqlite open Microsoft.Data.Sqlite diff --git a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj index dad9a03..489d298 100644 --- a/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj +++ b/src/Tests.CSharp/BitBadger.Documents.Tests.CSharp.csproj @@ -8,7 +8,6 @@ - diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index a189c86..c8d21d3 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -20,7 +20,6 @@ - -- 2.45.1 From a3e75bf42f952fddd4e8d435a0b7398ab656aaa8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 12:57:06 -0500 Subject: [PATCH 17/26] WIP on Postgres extensions --- .../BitBadger.Documents.Postgres.fsproj | 1 + src/Postgres/Extensions.fs | 333 ++++++++++++++++++ src/Sqlite/Extensions.fs | 1 - 3 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 src/Postgres/Extensions.fs diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index 246a808..5cd5b86 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -2,6 +2,7 @@ + diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs new file mode 100644 index 0000000..5a53a8f --- /dev/null +++ b/src/Postgres/Extensions.fs @@ -0,0 +1,333 @@ +namespace BitBadger.Documents.Postgres + +open Npgsql +open Npgsql.FSharp + +/// F# Extensions for the NpgsqlConnection type +[] +module Extensions = + + type NpgsqlConnection with + + /// Execute a query that returns a list of results + member conn.customList<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.list<'TDoc> query parameters mapFunc (Sql.existingConnection conn) + + /// Execute a query that returns one or no results; returns None if not found + member conn.customSingle<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.single<'TDoc> query parameters mapFunc (Sql.existingConnection conn) + + /// Execute a query that returns no results + member conn.customNonQuery query parameters = + WithProps.Custom.nonQuery query parameters (Sql.existingConnection conn) + + /// Execute a query that returns a scalar value + member conn.customScalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.scalar query parameters mapFunc (Sql.existingConnection conn) + + /// Create a document table + member conn.ensureTable name = + WithProps.Definition.ensureTable name (Sql.existingConnection conn) + + /// Create an index on documents in the specified table + member conn.ensureDocumentIndex name idxType = + WithProps.Definition.ensureDocumentIndex name idxType (Sql.existingConnection conn) + + /// Create an index on field(s) within documents in the specified table + member conn.ensureFieldIndex tableName indexName fields = + WithProps.Definition.ensureFieldIndex tableName indexName fields (Sql.existingConnection conn) + + /// Insert a new document + member conn.insert<'TDoc> tableName (document: 'TDoc) = + WithProps.Document.insert<'TDoc> tableName document (Sql.existingConnection 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) = + WithProps.Document.save<'TDoc> tableName document (Sql.existingConnection conn) + + /// Count all documents in a table + member conn.countAll tableName = + WithProps.Count.all tableName (Sql.existingConnection conn) + + /// Count matching documents using a JSON field comparison query (->> =) + member conn.countByField tableName fieldName op (value: obj) = + WithProps.Count.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Count matching documents using a JSON containment query (@>) + member conn.countByContains tableName criteria = + WithProps.Count.byContains tableName criteria (Sql.existingConnection conn) + + /// Count matching documents using a JSON Path match query (@?) + member conn.countByJsonPath tableName jsonPath = + WithProps.Count.byJsonPath tableName jsonPath (Sql.existingConnection conn) + + /// Determine if a document exists for the given ID + member conn.existsById tableName docId = + WithProps.Exists.byId tableName docId (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON field comparison query (->> =) + member conn.existsByField tableName fieldName op (value: obj) = + WithProps.Exists.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON containment query (@>) + member conn.existsByContains tableName criteria = + WithProps.Exists.byContains tableName criteria (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON Path match query (@?) + member conn.existsByJsonPath tableName jsonPath = + WithProps.Exists.byJsonPath tableName jsonPath (Sql.existingConnection conn) + + /// Retrieve all documents in the given table + member conn.findAll<'TDoc> tableName = + WithProps.Find.all<'TDoc> tableName (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 fieldName op (value: obj) = + WithProps.Find.byField<'TDoc> tableName fieldName op value (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 Path match query (@?) + member conn.findByJsonPath<'TDoc> 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 + member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) = + WithProps.Find.firstByField<'TDoc> tableName fieldName op value (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 Path match query (@?); returns None if not found + member conn.findFirstByJsonPath<'TDoc> tableName jsonPath = + WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) + + /// Update a full document + member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = + WithProps.Update.full tableName docId document (Sql.existingConnection conn) + + /// Update a full document + member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithProps.Update.fullFunc tableName idFunc document (Sql.existingConnection conn) + + /// Update a partial document + member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPartial) = + WithProps.Update.partialById tableName docId partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPartial) = + WithProps.Update.partialByField tableName fieldName op value partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON containment query in the WHERE clause (@>) + member conn.updatePartialByContains tableName (criteria: 'TCriteria) (partial: 'TPartial) = + WithProps.Update.partialByContains tableName criteria partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON Path match query in the WHERE clause (@?) + member conn.updatePartialByJsonPath tableName jsonPath (partial: 'TPartial) = + WithProps.Update.partialByJsonPath tableName jsonPath partial (Sql.existingConnection conn) + + /// Delete a document by its ID + 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 fieldName op (value: obj) = + WithProps.Delete.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Delete documents by matching a JSON containment query (@>) + member conn.deleteByContains tableName (criteria: 'TContains) = + WithProps.Delete.byContains tableName criteria (Sql.existingConnection conn) + + /// Delete documents by matching a JSON Path match query (@?) + member conn.deleteByJsonPath tableName path = + WithProps.Delete.byJsonPath tableName path (Sql.existingConnection conn) + + +open System.Runtime.CompilerServices + +/// C# extensions on the NpgsqlConnection type +type NpgsqlConnectionCSharpExtensions = + + /// Execute a query that returns a list of results + [] + static member inline CustomList<'TDoc>(conn, query, parameters, mapFunc: System.Func) = + WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn) + + /// Execute a query that returns one or no results; returns None if not found + [] + static member inline CustomSingle<'TDoc when 'TDoc: null>( + conn, query, parameters, mapFunc: System.Func) = + WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn) + + /// Execute a query that returns no results + [] + static member inline CustomNonQuery(conn, query, parameters) = + WithProps.Custom.nonQuery query parameters (Sql.existingConnection conn) + + /// Execute a query that returns a scalar value + [] + static member inline CustomScalar<'T when 'T: struct>( + conn, query, parameters, mapFunc: System.Func) = + WithProps.Custom.Scalar(query, parameters, mapFunc, Sql.existingConnection conn) + + /// Create a document table + [] + static member inline EnsureTable(conn, name) = + WithProps.Definition.ensureTable name (Sql.existingConnection conn) + + /// Create an index on documents in the specified table + [] + static member inline EnsureDocumentIndex(conn, name, idxType) = + WithProps.Definition.ensureDocumentIndex name idxType (Sql.existingConnection conn) + + /// Create an index on field(s) within documents in the specified table + [] + static member inline EnsureFieldIndex(conn, tableName, indexName, fields) = + WithProps.Definition.ensureFieldIndex tableName indexName fields (Sql.existingConnection conn) + + /// Insert a new document + [] + static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = + WithProps.Document.insert<'TDoc> tableName document (Sql.existingConnection 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) = + WithProps.Document.save<'TDoc> tableName document (Sql.existingConnection conn) + + /// Count all documents in a table + [] + static member inline CountAll(conn, tableName) = + WithProps.Count.all tableName (Sql.existingConnection conn) + + /// Count matching documents using a JSON field comparison query (->> =) + [] + static member inline CountByField(conn, tableName, fieldName, op, value: obj) = + WithProps.Count.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Count matching documents using a JSON containment query (@>) + [] + static member inline CountByContains(conn, tableName, criteria: 'TCriteria) = + WithProps.Count.byContains tableName criteria (Sql.existingConnection conn) + + /// Count matching documents using a JSON Path match query (@?) + [] + static member inline CountByJsonPath(conn, tableName, jsonPath) = + WithProps.Count.byJsonPath tableName jsonPath (Sql.existingConnection conn) + + /// Determine if a document exists for the given ID + [] + static member inline ExistsById(conn, tableName, docId) = + WithProps.Exists.byId tableName docId (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + static member inline ExistsByField(conn, tableName, fieldName, op, value: obj) = + WithProps.Exists.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON containment query (@>) + [] + static member inline ExistsByContains(conn, tableName, criteria: 'TCriteria) = + WithProps.Exists.byContains tableName criteria (Sql.existingConnection conn) + + /// Determine if documents exist using a JSON Path match query (@?) + [] + static member inline ExistsByJsonPath(conn, tableName, jsonPath) = + WithProps.Exists.byJsonPath tableName jsonPath (Sql.existingConnection conn) + + /// Retrieve all documents in the given table + [] + static member inline FindAll<'TDoc>(conn, tableName) = + WithProps.Find.All<'TDoc>(tableName, 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) = + WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, Sql.existingConnection conn) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + static member inline FindByField<'TDoc>(conn, tableName, fieldName, op, value: obj) = + WithProps.Find.ByField<'TDoc>(tableName, fieldName, op, value, 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 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 + [] + static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) = + WithProps.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, 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 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) + + /// Update a full document + [] + static member inline UpdateFull(conn, tableName, docId: 'TKey, document: 'TDoc) = + WithProps.Update.full tableName docId document (Sql.existingConnection conn) + + /// Update a full document + [] + static member inline UpdateFullFunc(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + WithProps.Update.FullFunc(tableName, idFunc, document, Sql.existingConnection conn) + + /// Update a partial document + [] + static member inline UpdatePartialById(conn, tableName, docId: 'TKey, partial: 'TPartial) = + WithProps.Update.partialById tableName docId partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + [] + static member inline UpdatePartialByField(conn, tableName, fieldName, op, value: obj, partial: 'TPartial) = + WithProps.Update.partialByField tableName fieldName op value partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON containment query in the WHERE clause (@>) + [] + static member inline UpdatePartialByContains(conn, tableName, criteria: 'TCriteria, partial: 'TPartial) = + WithProps.Update.partialByContains tableName criteria partial (Sql.existingConnection conn) + + /// Update partial documents using a JSON Path match query in the WHERE clause (@?) + [] + static member inline UpdatePartialByJsonPath(conn, tableName, jsonPath, partial: 'TPartial) = + WithProps.Update.partialByJsonPath tableName jsonPath partial (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, fieldName, op, value: obj) = + WithProps.Delete.byField tableName fieldName op value (Sql.existingConnection conn) + + /// Delete documents by matching a JSON containment query (@>) + [] + static member inline DeleteByContains(conn, tableName, criteria: 'TContains) = + WithProps.Delete.byContains tableName criteria (Sql.existingConnection conn) + + /// Delete documents by matching a JSON Path match query (@?) + [] + static member inline DeleteByJsonPath(conn, tableName, path) = + WithProps.Delete.byJsonPath tableName path (Sql.existingConnection conn) diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index bacd13b..57ed75f 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -100,7 +100,6 @@ module Extensions = open System.Runtime.CompilerServices /// C# extensions on the SqliteConnection type -[] type SqliteConnectionCSharpExtensions = /// Execute a query that returns a list of results -- 2.45.1 From 6e467ec8f3e4303690403c40e6ffd1af0cc08047 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 14:45:12 -0500 Subject: [PATCH 18/26] Add F# Postgres extension tests --- src/Postgres/Extensions.fs | 2 +- src/Tests.CSharp/PostgresDb.cs | 8 +- src/Tests/BitBadger.Documents.Tests.fsproj | 1 + src/Tests/PostgresExtensionTests.fs | 700 +++++++++++++++++++++ src/Tests/PostgresTests.fs | 5 +- src/Tests/Program.fs | 3 +- src/Tests/SqliteExtensionTests.fs | 12 +- src/Tests/Types.fs | 10 + 8 files changed, 726 insertions(+), 15 deletions(-) create mode 100644 src/Tests/PostgresExtensionTests.fs diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index 5a53a8f..e1bd553 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -3,7 +3,7 @@ namespace BitBadger.Documents.Postgres open Npgsql open Npgsql.FSharp -/// F# Extensions for the NpgsqlConnection type +/// F# Extensions for the NpgsqlConnection type [] module Extensions = diff --git a/src/Tests.CSharp/PostgresDb.cs b/src/Tests.CSharp/PostgresDb.cs index 2ee10e1..f695a04 100644 --- a/src/Tests.CSharp/PostgresDb.cs +++ b/src/Tests.CSharp/PostgresDb.cs @@ -1,4 +1,3 @@ -using BitBadger.Documents.Postgres; using Npgsql; using Npgsql.FSharp; using ThrowawayDb.Postgres; @@ -12,8 +11,15 @@ public class ThrowawayPostgresDb : IDisposable, IAsyncDisposable { private readonly ThrowawayDatabase _db; + /// + /// The connection string for the throwaway database + /// public string ConnectionString => _db.ConnectionString; + /// + /// Constructor + /// + /// The throwaway database which this instance will wrap public ThrowawayPostgresDb(ThrowawayDatabase db) { _db = db; diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index c8d21d3..9e753ad 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs new file mode 100644 index 0000000..8fdbacc --- /dev/null +++ b/src/Tests/PostgresExtensionTests.fs @@ -0,0 +1,700 @@ +module PostgresExtensionTests + +open BitBadger.Documents +open BitBadger.Documents.Postgres +open BitBadger.Documents.Tests +open Expecto +open Npgsql +open Types + +/// Open a connection to the throwaway database +let private mkConn (db: ThrowawayPostgresDb) = + let conn = new NpgsqlConnection(db.ConnectionString) + conn.Open() + conn + +/// Integration tests for the F# extensions on the NpgsqlConnection data type +let integrationTests = + let loadDocs (conn: NpgsqlConnection) = backgroundTask { + for doc in testDocuments do do! conn.insert PostgresDb.TableName doc + } + testList "Postgres.Extensions" [ + testList "customList" [ + testTask "succeeds when data is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.customList (Query.selectFromTable PostgresDb.TableName) [] fromData + Expect.equal (List.length docs) 5 "There should have been 5 documents returned" + } + testTask "succeeds when data is not found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = + conn.customList + $"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 "customSingle" [ + testTask "succeeds when a row is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.customSingle + $"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() + use conn = mkConn db + do! loadDocs conn + + let! doc = + conn.customSingle + $"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 "customNonQuery" [ + testTask "succeeds when operating on data" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.customNonQuery $"DELETE FROM {PostgresDb.TableName}" [] + + let! remaining = conn.countAll 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() + use conn = mkConn db + do! loadDocs conn + + do! conn.customNonQuery + $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" + [ "@path", Sql.string "$.NumValue ? (@ > 100)" ] + + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 5 "There should be 5 documents remaining in the table" + } + ] + testTask "scalar succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let! nbr = conn.customScalar "SELECT 5 AS test_value" [] (fun row -> row.int "test_value") + Expect.equal nbr 5 "The query should have returned the number 5" + } + testTask "ensureTable succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let tableExists () = + conn.customScalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists + let keyExists () = + conn.customScalar + "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! conn.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() + use conn = mkConn db + let indexExists () = + conn.customScalar + "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! conn.ensureTable "ensured" + do! conn.ensureDocumentIndex "ensured" Optimized + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } + testTask "ensureFieldIndex succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let indexExists () = + conn.customScalar + "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! conn.ensureTable "ensured" + do! conn.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } + testList "insert" [ + testTask "succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "turkey"; Sub = Some { Foo = "gobble"; Bar = "gobble" } } + do! conn.insert PostgresDb.TableName testDoc + let! after = conn.findAll PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "fails for duplicate key" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! conn.insert PostgresDb.TableName { emptyDoc with Id = "test" } + Expect.throws + (fun () -> + conn.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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! conn.save PostgresDb.TableName testDoc + let! after = conn.findAll PostgresDb.TableName + Expect.equal after [ testDoc ] "There should have been one document inserted" + } + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } + do! conn.insert PostgresDb.TableName testDoc + + let! before = conn.findById 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! conn.save PostgresDb.TableName upd8Doc + let! after = conn.findById 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" + } + ] + testTask "countAll succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! theCount = conn.countAll PostgresDb.TableName + Expect.equal theCount 5 "There should have been 5 matching documents" + } + testTask "countByField succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! theCount = conn.countByField PostgresDb.TableName "Value" EQ "purple" + Expect.equal theCount 2 "There should have been 2 matching documents" + } + testTask "countByContains succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! theCount = conn.countByContains PostgresDb.TableName {| Value = "purple" |} + Expect.equal theCount 2 "There should have been 2 matching documents" + } + testTask "countByJsonPath succeeds" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! theCount = conn.countByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 5)" + Expect.equal theCount 3 "There should have been 3 matching documents" + } + testList "existsById" [ + testTask "succeeds when a document exists" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsById 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() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsById PostgresDb.TableName "seven" + Expect.isFalse exists "There should not have been an existing document" + } + ] + testList "existsByField" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByField PostgresDb.TableName "Sub" EX "" + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when documents do not exist" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByField PostgresDb.TableName "NumValue" EQ "six" + Expect.isFalse exists "There should not have been existing documents" + } + ] + testList "existsByContains" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByContains PostgresDb.TableName {| NumValue = 10 |} + Expect.isTrue exists "There should have been existing documents" + } + testTask "succeeds when no matching documents exist" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByContains PostgresDb.TableName {| Nothing = "none" |} + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "existsByJsonPath" [ + testTask "succeeds when documents exist" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByJsonPath 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() + use conn = mkConn db + do! loadDocs conn + + let! exists = conn.existsByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 1000)" + Expect.isFalse exists "There should not have been any existing documents" + } + ] + testList "findAll" [ + testTask "succeeds when there is data" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + + do! conn.insert PostgresDb.TableName { Foo = "one"; Bar = "two" } + do! conn.insert PostgresDb.TableName { Foo = "three"; Bar = "four" } + 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" + } + testTask "succeeds when there is no data" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let! results = conn.findAll PostgresDb.TableName + Expect.equal results [] "There should have been no documents returned" + } + ] + testList "findById" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findById 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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findById PostgresDb.TableName "three hundred eighty-seven" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "findByField" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByField PostgresDb.TableName "Value" EQ "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() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByField PostgresDb.TableName "Value" EQ "mauve" + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "findByContains" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByContains 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() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByContains PostgresDb.TableName {| Value = "mauve" |} + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "findByJsonPath" [ + testTask "succeeds when documents are found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByJsonPath 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() + use conn = mkConn db + do! loadDocs conn + + let! docs = conn.findByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" + Expect.isEmpty docs "There should have been no documents returned" + } + ] + testList "findFirstByField" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByField PostgresDb.TableName "Value" EQ "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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByField PostgresDb.TableName "Value" EQ "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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByField PostgresDb.TableName "Value" EQ "absent" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "findFirstByContains" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByContains 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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByContains 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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByContains PostgresDb.TableName {| Value = "absent" |} + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "findFirstByJsonPath" [ + testTask "succeeds when a document is found" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByJsonPath 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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByJsonPath 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() + use conn = mkConn db + do! loadDocs conn + + let! doc = conn.findFirstByJsonPath PostgresDb.TableName """$.Id ? (@ == "nope")""" + Expect.isNone doc "There should not have been a document returned" + } + ] + testList "updateFull" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } + do! conn.updateFull PostgresDb.TableName "one" testDoc + let! after = conn.findById 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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFull + PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } + } + ] + testList "updateFullFunc" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.updateFullFunc + PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + let! after = conn.findById 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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updateFullFunc + PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + } + ] + testList "updatePartialById" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.updatePartialById PostgresDb.TableName "one" {| NumValue = 44 |} + let! after = conn.findById 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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialById PostgresDb.TableName "test" {| Foo = "green" |} + } + ] + testList "updatePartialByField" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.updatePartialByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + let! after = Count.byField PostgresDb.TableName "NumValue" EQ "77" + Expect.equal after 2 "There should have been 2 documents returned" + } + testTask "succeeds when no document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + } + ] + testList "updatePartialByContains" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.updatePartialByContains 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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + } + ] + testList "updatePartialByJsonPath" [ + testTask "succeeds when a document is updated" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.updatePartialByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} + let! after = conn.countByJsonPath 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() + use conn = mkConn db + let! before = conn.countAll PostgresDb.TableName + Expect.equal before 0 "There should have been no documents returned" + + // This not raising an exception is the test + do! conn.updatePartialByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} + } + ] + testList "deleteById" [ + testTask "succeeds when a document is deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteById PostgresDb.TableName "four" + let! remaining = conn.countAll 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() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteById PostgresDb.TableName "thirty" + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "deleteByField" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByField PostgresDb.TableName "Value" EQ "purple" + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByField PostgresDb.TableName "Value" EQ "crimson" + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "deleteByContains" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByContains PostgresDb.TableName {| Value = "purple" |} + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByContains PostgresDb.TableName {| Value = "crimson" |} + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + testList "deleteByJsonPath" [ + testTask "succeeds when documents are deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByJsonPath PostgresDb.TableName """$.Sub.Foo ? (@ == "green")""" + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 3 "There should have been 3 documents remaining" + } + testTask "succeeds when documents are not deleted" { + use db = PostgresDb.BuildDb() + use conn = mkConn db + do! loadDocs conn + + do! conn.deleteByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 100)" + let! remaining = conn.countAll PostgresDb.TableName + Expect.equal remaining 5 "There should have been 5 documents remaining" + } + ] + ] + |> testSequenced diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 46fbce0..494c896 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -240,9 +240,8 @@ let integrationTests = } ] testTask "scalar succeeds" { - use db = PostgresDb.BuildDb() - - let! nbr = Custom.scalar $"SELECT 5 AS test_value" [] (fun row -> row.int "test_value") + 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" } ] diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 30fdeef..e496e3a 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -8,8 +8,9 @@ let allTests = CommonCSharpTests.Unit PostgresTests.all PostgresCSharpTests.All + PostgresExtensionTests.integrationTests SqliteTests.all - testSequenced SqliteExtensionTests.integrationTests + SqliteExtensionTests.integrationTests SqliteCSharpTests.All testSequenced SqliteCSharpExtensionTests.Integration ] diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index dedf167..2d43813 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -9,17 +9,10 @@ open Types /// Integration tests for the F# extensions on the SqliteConnection data type 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 + for doc in testDocuments do do! insert SqliteDb.TableName doc } - testList "Extensions" [ + testList "Sqlite.Extensions" [ testTask "ensureTable succeeds" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () @@ -465,3 +458,4 @@ let integrationTests = Configuration.useConnectionString "data source=:memory:" } ] + |> testSequenced diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index e696a2a..de2abca 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -10,4 +10,14 @@ type JsonDocument = NumValue: int Sub: SubDocument option } +/// An empty JsonDocument let emptyDoc = { Id = ""; Value = ""; NumValue = 0; Sub = None } + +/// Documents to use for testing +let testDocuments = [ + { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } + { 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 } +] -- 2.45.1 From 12b4c6dd431c3f57cfa2ccd7b11c9f2bff3563fe Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 16:38:25 -0500 Subject: [PATCH 19/26] Add C# Postgres extension tests --- .../PostgresCSharpExtensionTests.cs | 811 ++++++++++++++++++ .../SqliteCSharpExtensionTests.cs | 2 +- src/Tests/PostgresExtensionTests.fs | 4 +- src/Tests/Program.fs | 3 +- 4 files changed, 816 insertions(+), 4 deletions(-) create mode 100644 src/Tests.CSharp/PostgresCSharpExtensionTests.cs diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs new file mode 100644 index 0000000..103e890 --- /dev/null +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -0,0 +1,811 @@ +using Expecto.CSharp; +using Expecto; +using BitBadger.Documents.Postgres; +using Npgsql; + +namespace BitBadger.Documents.Tests.CSharp; + +using static CommonExtensionsAndTypesForNpgsqlFSharp; +using static Runner; + +/// +/// C# tests for the extensions on the NpgsqlConnection type +/// +public class PostgresCSharpExtensionTests +{ + private static Task LoadDocs() => PostgresCSharpTests.LoadDocs(); + + /// + /// Create a connection to the throwaway database + /// + /// The throwaway database for which a connection should be made + /// An open connection to the throwaway database + private static NpgsqlConnection MkConn(ThrowawayPostgresDb db) + { + var conn = new NpgsqlConnection(db.ConnectionString); + conn.Open(); + return conn; + } + + /// + /// Integration tests for the SQLite extension methods + /// + [Tests] + public static readonly Test Integration = TestList("Postgres.C#.Extensions", new[] + { + TestList("CustomList", new[] + { + 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, + 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 using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.CustomList( + $"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("CustomSingle", new[] + { + TestCase("succeeds when a row is found", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + 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); + 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 using var conn = MkConn(db); + 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); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("CustomNonQuery", new[] + { + TestCase("succeeds when operating on data", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName}", Parameters.None); + + var remaining = await conn.CountAll(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 using var conn = MkConn(db); + await LoadDocs(); + + await conn.CustomNonQuery($"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + new[] { 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(); + await using var conn = MkConn(db); + var nbr = await conn.CustomScalar("SELECT 5 AS test_value", Parameters.None, row => row.@int("test_value")); + Expect.equal(nbr, 5, "The query should have returned the number 5"); + }), + TestCase("EnsureTable succeeds", async () => + { + 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(); + 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(); + 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(); + 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(); + Expect.isFalse(exists, "The index should not exist already"); + + await conn.EnsureTable("ensured"); + await conn.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(); + 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(); + Expect.isFalse(exists, "The index should not exist already"); + + await conn.EnsureTable("ensured"); + await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); + exists = await indexExists(); + Expect.isTrue(exists, "The index should now exist"); + }), + TestList("Insert", new[] + { + TestCase("succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await conn.Insert(PostgresDb.TableName, + new JsonDocument { Id = "turkey", Sub = new() { Foo = "gobble", Bar = "gobble" } }); + var after = await conn.FindAll(PostgresDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("fails for duplicate key", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "test" }); + try + { + await conn.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 + } + }) + }), + TestList("save", new[] + { + TestCase("succeeds when a document is inserted", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await conn.Save(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + var after = await conn.FindAll(PostgresDb.TableName); + Expect.equal(after.Count, 1, "There should have been one document inserted"); + }), + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await conn.Insert(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "a", Bar = "b" } }); + + var before = await conn.FindById(PostgresDb.TableName, "test"); + Expect.isNotNull(before, "There should have been a document returned"); + Expect.equal(before.Id, "test", "The document is not correct"); + + await conn.Save(PostgresDb.TableName, + new JsonDocument { Id = "test", Sub = new() { Foo = "c", Bar = "d" } }); + var after = await conn.FindById(PostgresDb.TableName, "test"); + 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(); + await using var conn = MkConn(db); + await LoadDocs(); + + var theCount = await conn.CountAll(PostgresDb.TableName); + Expect.equal(theCount, 5, "There should have been 5 matching documents"); + }), + TestCase("CountByField succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var theCount = await conn.CountByField(PostgresDb.TableName, "Value", Op.EQ, "purple"); + Expect.equal(theCount, 2, "There should have been 2 matching documents"); + }), + TestCase("CountByContains succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var theCount = await conn.CountByContains(PostgresDb.TableName, new { Value = "purple" }); + Expect.equal(theCount, 2, "There should have been 2 matching documents"); + }), + TestCase("CountByJsonPath succeeds", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var theCount = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 5)"); + Expect.equal(theCount, 3, "There should have been 3 matching documents"); + }), + TestList("ExistsById", new[] + { + TestCase("succeeds when a document exists", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsById(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 using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsById(PostgresDb.TableName, "seven"); + Expect.isFalse(exists, "There should not have been an existing document"); + }) + }), + TestList("ExistsByField", new[] + { + 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, "Sub", Op.EX, ""); + Expect.isTrue(exists, "There should have been existing documents"); + }), + TestCase("succeeds when documents do not exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsByField(PostgresDb.TableName, "NumValue", Op.EQ, "six"); + Expect.isFalse(exists, "There should not have been existing documents"); + }) + }), + TestList("ExistsByContains", new[] + { + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsByContains(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 using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsByContains(PostgresDb.TableName, new { Nothing = "none" }); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }), + TestList("ExistsByJsonPath", new[] + { + TestCase("succeeds when documents exist", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsByJsonPath(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 using var conn = MkConn(db); + await LoadDocs(); + + var exists = await conn.ExistsByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 1000)"); + Expect.isFalse(exists, "There should not have been any existing documents"); + }) + }), + TestList("FindAll", new[] + { + TestCase("succeeds when there is data", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + + await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "one" }); + await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "three" }); + await conn.Insert(PostgresDb.TableName, new JsonDocument { Id = "five" }); + + var results = await conn.FindAll(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(); + await using var conn = MkConn(db); + var results = await conn.FindAll(PostgresDb.TableName); + Expect.isEmpty(results, "There should have been no documents returned"); + }) + }), + TestList("FindById", new[] + { + 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.FindById(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 using var conn = MkConn(db); + await LoadDocs(); + + 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[] + { + 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, "Value", Op.EQ, "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 using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByField(PostgresDb.TableName, "Value", Op.EQ, "mauve"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindByContains", new[] + { + 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.FindByContains(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 using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByContains(PostgresDb.TableName, new { Value = "mauve" }); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindByJsonPath", new[] + { + 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.FindByJsonPath(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 using var conn = MkConn(db); + await LoadDocs(); + + var docs = await conn.FindByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)"); + Expect.isEmpty(docs, "There should have been no documents returned"); + }) + }), + TestList("FindFirstByField", new[] + { + 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, "Value", Op.EQ, "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 using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByField(PostgresDb.TableName, "Value", Op.EQ, "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 using var conn = MkConn(db); + await LoadDocs(); + + var doc = await conn.FindFirstByField(PostgresDb.TableName, "Value", Op.EQ, "absent"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("FindFirstByContains", new[] + { + 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.FindFirstByContains(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 using var conn = MkConn(db); + await LoadDocs(); + + 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"); + }), + 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.FindFirstByContains(PostgresDb.TableName, new { Value = "absent" }); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("FindFirstByJsonPath", new[] + { + 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.FindFirstByJsonPath(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 using var conn = MkConn(db); + await LoadDocs(); + + 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"); + }), + 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.FindFirstByJsonPath(PostgresDb.TableName, "$.Id ? (@ == \"nope\")"); + Expect.isNull(doc, "There should not have been a document returned"); + }) + }), + TestList("UpdateFull", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdateFull(PostgresDb.TableName, "one", + new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); + var after = await conn.FindById(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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFull(PostgresDb.TableName, "test", + new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); + }) + }), + TestList("UpdateFullFunc", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdateFullFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + var after = await conn.FindById(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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdateFullFunc(PostgresDb.TableName, doc => doc.Id, + new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); + }) + }), + TestList("UpdatePartialById", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdatePartialById(PostgresDb.TableName, "one", new { NumValue = 44 }); + var after = await conn.FindById(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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialById(PostgresDb.TableName, "test", new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByField", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdatePartialByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + var after = await conn.CountByField(PostgresDb.TableName, "NumValue", Op.EQ, "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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", + new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByContains", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdatePartialByContains(PostgresDb.TableName, new { Value = "purple" }, + new { NumValue = 77 }); + var after = await conn.CountByContains(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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByContains(PostgresDb.TableName, new { Value = "burgundy" }, + new { Foo = "green" }); + }) + }), + TestList("UpdatePartialByJsonPath", new[] + { + TestCase("succeeds when a document is updated", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.UpdatePartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", + new { NumValue = 1000 }); + var after = await conn.CountByJsonPath(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(); + await using var conn = MkConn(db); + var before = await conn.CountAll(PostgresDb.TableName); + Expect.equal(before, 0, "There should have been no documents returned"); + + // This not raising an exception is the test + await conn.UpdatePartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + }) + }), + TestList("DeleteById", new[] + { + TestCase("succeeds when a document is deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteById(PostgresDb.TableName, "four"); + var remaining = await conn.CountAll(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 using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteById(PostgresDb.TableName, "thirty"); + var remaining = await conn.CountAll(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByField", new[] + { + 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, "Value", Op.NE, "purple"); + var remaining = await conn.CountAll(PostgresDb.TableName); + Expect.equal(remaining, 2, "There should have been 2 documents remaining"); + }), + TestCase("succeeds when documents are not deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteByField(PostgresDb.TableName, "Value", Op.EQ, "crimson"); + var remaining = await conn.CountAll(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByContains", new[] + { + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteByContains(PostgresDb.TableName, new { Value = "purple" }); + var remaining = await conn.CountAll(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 using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteByContains(PostgresDb.TableName, new { Value = "crimson" }); + var remaining = await conn.CountAll(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + }), + TestList("DeleteByJsonPath", new[] + { + TestCase("succeeds when documents are deleted", async () => + { + await using var db = PostgresDb.BuildDb(); + await using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteByJsonPath(PostgresDb.TableName, "$.Sub.Foo ? (@ == \"green\")"); + var remaining = await conn.CountAll(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 using var conn = MkConn(db); + await LoadDocs(); + + await conn.DeleteByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 100)"); + var remaining = await conn.CountAll(PostgresDb.TableName); + Expect.equal(remaining, 5, "There should have been 5 documents remaining"); + }) + }), + }); +} diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 9015d26..54123ef 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -18,7 +18,7 @@ public static class SqliteCSharpExtensionTests /// Integration tests for the SQLite extension methods /// [Tests] - public static readonly Test Integration = TestList("Extensions", new[] + public static readonly Test Integration = TestList("Sqlite.C#.Extensions", new[] { TestList("CustomSingle", new[] { diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs index 8fdbacc..220463a 100644 --- a/src/Tests/PostgresExtensionTests.fs +++ b/src/Tests/PostgresExtensionTests.fs @@ -563,7 +563,7 @@ let integrationTests = do! loadDocs conn do! conn.updatePartialByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} - let! after = Count.byField PostgresDb.TableName "NumValue" EQ "77" + let! after = conn.countByField PostgresDb.TableName "NumValue" EQ "77" Expect.equal after 2 "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { @@ -583,7 +583,7 @@ let integrationTests = do! loadDocs conn do! conn.updatePartialByContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} - let! after = Count.byContains PostgresDb.TableName {| NumValue = 77 |} + let! after = conn.countByContains PostgresDb.TableName {| NumValue = 77 |} Expect.equal after 2 "There should have been 2 documents returned" } testTask "succeeds when no document is updated" { diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index e496e3a..7b1bdf2 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -9,9 +9,10 @@ let allTests = PostgresTests.all PostgresCSharpTests.All PostgresExtensionTests.integrationTests + testSequenced PostgresCSharpExtensionTests.Integration SqliteTests.all - SqliteExtensionTests.integrationTests SqliteCSharpTests.All + SqliteExtensionTests.integrationTests testSequenced SqliteCSharpExtensionTests.Integration ] [] -- 2.45.1 From 9c6741d0b04d86b29f60a735c6d580d7124ae7db Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 27 Dec 2023 22:50:21 -0500 Subject: [PATCH 20/26] WIP on READMEs and packaging --- src/Common/BitBadger.Documents.Common.fsproj | 7 ++ src/Common/README.md | 17 +++ src/Directory.Build.props | 6 +- .../BitBadger.Documents.Postgres.fsproj | 7 ++ src/Postgres/README.md | 101 ++++++++++++++++++ src/Sqlite/BitBadger.Documents.Sqlite.fsproj | 8 +- src/Sqlite/README.md | 101 ++++++++++++++++++ src/icon.png | Bin 0 -> 25176 bytes 8 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 src/Common/README.md create mode 100644 src/Postgres/README.md create mode 100644 src/Sqlite/README.md create mode 100644 src/icon.png diff --git a/src/Common/BitBadger.Documents.Common.fsproj b/src/Common/BitBadger.Documents.Common.fsproj index cfcbca0..2c3f063 100644 --- a/src/Common/BitBadger.Documents.Common.fsproj +++ b/src/Common/BitBadger.Documents.Common.fsproj @@ -1,7 +1,14 @@  + + Initial release (RC 1) + JSON Document SQL + + + + diff --git a/src/Common/README.md b/src/Common/README.md new file mode 100644 index 0000000..7047424 --- /dev/null +++ b/src/Common/README.md @@ -0,0 +1,17 @@ +# BitBadger.Documents.Common + +This package provides common definitions and functionality for `BitBadger.Documents` implementations. These libraries provide a document storage view over relational databases, while also providing convenience functions for relational usage as well. This enables a hybrid approach to data storage, allowing the user to use documents where they make sense, while streamlining traditional ADO.NET functionality where relational data is required. +- `BitBadger.Documents.Postgres` ([NuGet](https://www.nuget.org/packages/BitBadger.Documents.Postgres/)) provides a PostgreSQL implementation. +- `BitBadger.Documents.Sqlite` ([NuGet](https://www.nuget.org/packages/BitBadger.Documents.Sqlite/)) provides a SQLite implementation + +## Features + +- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries) +- Accesses documents as your domain models (POCOs) +- Uses `Task`-based async for all data access functions +- Uses building blocks for more complex queries + +## Getting Started + +Install the library of your choice and follow its README; also, the [project site](https://bitbadger.solutions/open-source/relational-documents/) has complete documentation. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4c182c4..d587c48 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,17 +7,15 @@ 3.0.0.0 3.0.0 rc-1 - Initial release with F# support danieljsummers Bit Badger Solutions README.md icon.png - https://bitbadger.solutions/open-source/sqlite-documents/ + https://bitbadger.solutions/open-source/relational-documents/ false - https://github.com/bit-badger/BitBadger.Sqlite.Documents + https://github.com/bit-badger/BitBadger.Documents Git MIT License MIT - SQLite JSON document diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index 5cd5b86..7a7af5b 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -1,8 +1,15 @@  + + Initial release; migrated from BitBadger.Npgsql.Documents, with field and extension additions (RC 1) + JSON Document PostgreSQL Npgsql + + + + diff --git a/src/Postgres/README.md b/src/Postgres/README.md new file mode 100644 index 0000000..039856b --- /dev/null +++ b/src/Postgres/README.md @@ -0,0 +1,101 @@ +# BitBadger.Documents.Postgres + +This package provides a lightweight document library backed by [PostgreSQL](https://www.postgresql.org). It also provides streamlined functions for traditional ADO.NET functionality where relational data is required. Both C# and F# have first-class implementations. + +## Features + +- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries) +- Access documents as your domain models (POCOs) +- Use `Task`-based async for all data access functions +- Use building blocks for more complex queries + +## Getting Started + +Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library: + +```csharp +// C# +using BitBadger.Documents.Postgres; + +//... +// Do not use "using" here; the library will handle disposing this instance +var data = new NpgsqlDataSourceBuilder("connection-string").Build(); +Postgres.Configuration.UseDataSource(data); +``` + +```fsharp +// F# +open BitBadger.Documents.Postgres + +// ... +// Do not use "use" here; the library will handle disposing this instance +let dataSource = // same as above .... + +Configuration.useDataSource dataSource +// ... +``` + +By default, the library uses a `System.Text.Json`-based serializer configured to use the `FSharp.SystemTextJson` converter. To provide a different serializer (different options, more converters, etc.), construct it to implement `IDocumentSerializer` and provide it via `Configuration.useSerializer`. If custom serialization makes the serialized Id field not be `Id`, that will also need to be configured. + +## Using + +Retrieve all customers: + +```csharp +// C#; parameter is table name +// Find.All type signature is Func>> +var customers = await Find.All("customer"); +``` + +```fsharp +// F# +// Find.all type signature is string -> Task<'TDoc list> +let! customers = Find.all "customer" +``` + +Select a customer by ID: + +```csharp +// C#; parameters are table name and ID +// Find.ById type signature is Func> +var customer = await Find.ById("customer", "123"); +``` +```fsharp +// F# +// Find.byId type signature is string -> 'TKey -> Task<'TDoc option> +let! customer = Find.byId "customer" "123" +``` +_(keys are treated as strings in the database)_ + +Count customers in Atlanta (using JSON containment): + +```csharp +// C#; parameters are table name and object for containment query +// Count.ByContains type signature is Func +var customerCount = await Count.ByContains("customer", new { City = "Atlanta" }); +``` + +```fsharp +// F# +// Count.byContains type signature is string -> 'TCriteria -> Task +let! customerCount = Count.byContains "customer" {| City = "Atlanta" |} +``` + +Delete customers in Chicago: _(no offense, Second City; just an example...)_ + +```csharp +// C#; parameters are table name and JSON Path expression +// Delete.ByJsonPath type signature is Func +await Delete.ByJsonPath("customer", "$.City ? (@ == \"Chicago\")"); +``` + +```fsharp +// F# +// Delete.byJsonPath type signature is string -> string -> Task +do! Delete.byJsonPath "customer" """$.City ? (@ == "Chicago")""" +``` + +## More Information + +The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library. diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index 103f75b..864b7a4 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -1,12 +1,18 @@  + + Initial release; SQLite document implementation similar to BitBadger.Npgsql.Documents (RC 1) + JSON Document SQLite + + + + - diff --git a/src/Sqlite/README.md b/src/Sqlite/README.md new file mode 100644 index 0000000..b79958f --- /dev/null +++ b/src/Sqlite/README.md @@ -0,0 +1,101 @@ +# BitBadger.Documents.Sqlite + +This package provides a lightweight document library backed by [SQLite](https://www.sqlite.org). It also provides streamlined functions for traditional ADO.NET functionality where relational data is required. Both C# and F# have first-class implementations. + +## Features + +- Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Address documents via ID or via comparison on any field +- Access documents as your domain models (POCOs) +- Use `Task`-based async for all data access functions +- Use building blocks for more complex queries + +## Getting Started + +Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library: + +```csharp +// C# +using BitBadger.Documents.Sqlite; + +//... +Sqlite.Configuration.UseConnectionString("connection-string"); + +// A new, open connection to the database can be obtained via +// Sqlite.Configuration.DbConn() +``` + +```fsharp +// F# +open BitBadger.Documents.Sqlite + +// ... +Configuration.useConnectionString "connection-string" + +// A new, open connection to the database can be obtained via +// Configuration.dbConn () +``` + +By default, the library uses a `System.Text.Json`-based serializer configured to use the `FSharp.SystemTextJson` converter. To provide a different serializer (different options, more converters, etc.), construct it to implement `IDocumentSerializer` and provide it via `Configuration.useSerializer`. If custom serialization makes the serialized Id field not be `Id`, that will also need to be configured. + +## Using + +Retrieve all customers: + +```csharp +// C#; parameter is table name +// Find.All type signature is Func>> +var customers = await Find.All("customer"); +``` + +```fsharp +// F# +// Find.all type signature is string -> Task<'TDoc list> +let! customers = Find.all "customer" +``` + +Select a customer by ID: + +```csharp +// C#; parameters are table name and ID +// Find.ById type signature is Func> +var customer = await Find.ById("customer", "123"); +``` +```fsharp +// F# +// Find.byId type signature is string -> 'TKey -> Task<'TDoc option> +let! customer = Find.byId "customer" "123" +``` +_(keys are treated as strings in the database)_ + +Count customers in Atlanta: + +```csharp +// C#; parameters are table name, field, operator, and value +// Count.ByField type signature is Func> +var customerCount = await Count.ByField("customer", "City", Op.EQ, "Atlanta"); +``` + +```fsharp +// F# +// Count.byField type signature is string -> string -> Op -> obj -> Task +let! customerCount = Count.byField "customer" "City" EQ "Atlanta" +``` + +Delete customers in Chicago: _(no offense, Second City; just an example...)_ + +```csharp +// C#; parameters are same as above, except return is void +// Delete.ByField type signature is Func +await Delete.ByField("customer", "City", Op.EQ, "Chicago"); +``` + +```fsharp +// F# +// Delete.byField type signature is string -> string -> Op -> obj -> Task +do! Delete.byField "customer" "City" EQ "Chicago" +``` + +## More Information + +The [project site](https://bitbadger.solutions/open-source/relational-documents/) has full details on how to use this library. diff --git a/src/icon.png b/src/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..da6be2aecde37d3d614f3314a70bcbf0b1b2182f GIT binary patch literal 25176 zcmeFXWpEr#vNkGaW@ffVFk)tAS8{FrD)Xt#>aOXGQdX2gMj$`{0|P^rkrr3|_%-|M0|)c*?YNhw1_nm; z>aC{bs%q>(=IHETZeanG;F{mh14!@s9bUaWVXmY!03In<7QOkVltw_qQiHN~z%g1rx( z8n(jM)|Ibk;U75XUieZqCLhRS-(LN%0?L9Mib)GM+WH8dUaqt!APx8j`@+@evM zerc}N7I8a@X5uYk?5=Y?YAHR}{o0}Hh(Dd1sq3(d&whJ*QK1X(jkEW9;N^5kKuhdq zdY0@(;>~3Y*S14+r0yDGM4e){`k{*#gew^ZRyPOYJf zMBmpHvqfZWMWIfln2tL|>n_aKr-z3xKM5L)78pN?6JYY?(MUQ6B$DZg!7|Ce`VWqz z{o?G$x;YWI(CdfLnhrZ?Y94vnBf`{XRW3gl6c=g;l>5faduj@Yzphp<Xs;nt!sU zdAj!%0r6GKD@|Qf*RMP7(?!SPO!v>O^-EYyzJVOig>U6K-fO3Z8a~s{2?6gxOB*&E z0f17($uCRXIuqsawA?aH_%L@gyo?5B5JYQ}=51YgJ&d&3t1z!&2d8Gq)9EQA4MrO? z<@igH?j_Y+;Z*~**hf{h&v_tJFSDk`7iq$mcx|zOkKB>R>7ksZ!D`dnr`trkH?JU_DIXnnw`=5_> zbbf%GPl?+yXQQZxZ&wdXk1fp}c z97lO!e1;L7vf`>^!PGvuPBob5AYSgt*L`6$%}i<&@G~EghRBd+IG&Mo|J9MauDy=3 z0Q1KacK2nBh4xmjy-Z*c+XeOa%TEr6D~rdjM4mMlU}^E!mC||_ftv5;+Kt=0Nmizs zl>1gxA($>t((f>e-&*r&WYY*aCXq_wP;vX05xg@J z3n57ZizY}Efs*?ijKUE1)nzBsD3}z}c!8d)(w_}9`%|;n`G0gz2MvItpDbZoqu>lnk%vS$|p=3AWv&m!_z)? zS0Ya%;7|`g5Nh|6giSJBH^^iN3~{-WWX+qrGe4S5T-C7T!KeEb>{93`={zkE4BU-m ziiU*k)FMyq@DHtFe*`+~5aCRVD6d^r5_IobiKN3~*-#E)aTVLq@6m5cue3N>(*mF_ zB;{*O7{mvBkvE7VX_TyXwRZC>F9x9`H12%{0k*P(&C2tT<>t@R%cv%UX zG(dBdCAz*aS8e2ea>Oi`dL{*o3 z$lS6RzZIQk|iChHxqB;f+gRWa#M#*lWxsGf9${&6Xi7mB83Y z?HgZ4txv}WO$oXPGI+ihQ@FrSok&l;@B~u@VdPKV!1a~+hu}s$`FN|kBiWLAJ%aY9 zXJ3a;?a9UCKeL4+do<77e3;wy9i81F}%}@Lz z;3!iW9F6Hg_?-Bh(WedEIfqp^ zuC9QGM-(xam}PnOmc7~~*@+6;Hc4nSb~2pK*yc5Y&EKo)XBU+%!XNt{>DA1>GXy7xRr$=U_(;ATuv>aX|6jlrdU`t7@oXZh)5Sd>b~ zq6yMOXtj^thDBEnBKkKC1W;Id|FJc+KrVdz^OgdrV#J(TnM&k<@iNWvvXhrhrH)GW z58u~gQ{OrMoy_{^CUWJniuo%`nb~G+&cClK1eHeNC7|!#?@vPf4z_^-D**kiqp)au zTB!Eh`i2`G>tHm&Yk-@!$3oK+``f0Kg%k8V9rKxj;;`PSDgNcYz4LO_<%T_r|6Muh zth|5G#8!+RV4@V18j-ozRvF@#hFTHc^Q9g!E_xyhs` z@TOKL&O~=4R}Rx`%-7qw?L^HV=3NMyRTPE?gy-SzG4LfeLj{t!-+4jw|uf)}n*rq{}v z;@DL~6{wU>+t8K@_K1@Vfe8s+q_h(BZ29_Z9^n%c1SzFI1l7(A7U&#Wo_SwBpa=$h zqScgvoQGG6P%nh|+Wa)Z^8l{A33!h`Q-8nP;L=-M=`c7N231;#7-#muEEtsF4+j0= z#NFcCW2SsZ8hkx{QONDyHg36+I`>g~z!HE81UQ6z>QCO)+zK=noVk<|FOD_Ir??Bv z6LDKF3@YaVng-GT%@8x)pc2eHy0l1?@_wR z#xloU2M8Luy=LMR@n{MPFat%ycr%gtdvD<-xprNGz&=Z`M{QL;p4jlB{SHD>k_o#v zr>tzO3nB_{(%$TkgAH;UUrY>{DR7q59x50>U1|z_v#7Ho5J9#j{n_t0;Tns{+BHO$ zcGof>&Ixs)w#uk_g5N+;vZ&YSAajdjX20!scC_*K{{g$37T#5G!D zsIJ9_>P-Yo%-6+9b>FoBf1M^bRN4od^Td`A(-p6WNttm>BRM!|041#+j-&zaeqYC& z9@`los|~gP9qKp+W2YY>(~2Lihr3jC9*TZSUo*(m6;)I_j*(3F#3`QvYz-QFa%svw zN`}90Y%pvpxC${lu)hpRH!bHYj6W3jGSNjwVlfV@G&D&PBtOzIN0sdtU|I8lvhpm( zP0YNhHc{)Kw(waMH#hqFv+FpC*?C9glH##q3sbr6fkFq7#<{D{X4S30>H?Pt0$T_= ziOk_uXzz~bov|^^#u_l4W}?pqjFznXOvBRMfe%+|+({mIhuU@_q)CABLtwc71z_-I z_2q}x**Jvk6-g$Kfe3ox0m=_BZP%~aY1!(;h^wE8G5IheO>g2a#{nhomgF-~ma#Ma zwA<7?sW0N?B5(s1<^vojta%rFBrJtIASUO8u)V$T0(kC;`=tEk+eiP3IjZsKr z#CWd)*ajTRK23%#Btr6B>mzL_X)T6KoM|nJPf62BKI35Yge39DQWc*gIE1rot0Wg@ z6j{S5B1gFgCrX_aLm`kjG7-#JNgddxsFXQYA&nGy2JzrN~XP*JRI# z4?rg@V)8kIKozpXGsE8F31(=paC{O>z|}Z74xCebDD%`0pSv3Zr)=F6FLVM$aXxdCKhi`634sF@{va|K?JlQ*iv^}a z_@Y6v1nF?iJyrVR5DpSm?to}Vbwr{&mwc27 zV_87N#FZF?jMGSjK!>=my)mX@pXpm&hX9tc1<2(r!X;f^oyFqgbOe$KtWBtTH>YuStqk-;76S9v%8@-y*dl`Ox6K3;Pv= z4jR~j0-m(P;v(mN4H?`hJbsz~xl@EAtLNjstK*xwz$CV`eKob}LxCZWh9Daq1a#AR zUQzt|w~mYEvteNZ9$ykq->ggfm>)X61?PM~+v`jPbKO~9Wu%u?q)FJetVjBCG zqR$u#9VC$vA*p@3*HW=y(+LauL=J=ALZJj2sY~6evmVIQjC%_{$GY2!P<8LaG8P>i#pbL6LEdl3Oscz}>p&D+;^vpyciKuu@C3q1gsi3~(nC6(| zT=ffy^;Zb5gom=LcVopn%H<{F6&7%T0&g)x3UK3VKx}Y7sGO0{CWekZf=f?|P^|dK z1Irx6S_n6#E3Ac6NeXB8)f+dHO~%#4e|}C2vJC4m9#sY=dEMIjj}x0+#TN%M4&<|Q z%0%o~9r*p=Kv^3uXHlJBE9BCV4y)8GcO&=_`ZYyaYnDJI0)5L(u;mj`LJ7*Mkk0Rg zEO~%1A*Q}jC_~)t39b$vSi1-$Zu{YZ+e)nvP(nQ5J0-77+<5IU3xtRb6T^k~IGt#^ z%)lg1vM~Ma)uLsH#?HnJOoMkIgjG)*O$Lz+66ogvpO+QuvQ{kxxMy-OkBvQ{S#Y_xT*Sn^JPZYdTe$`trpSYeYRUZO8}3f?LZ9x9T?fBG9`Z2Lh- z`L6D%69@3U;!ZhU$K}Ob4<@Tk}WUY=y+_9Aoj##%ssZAN01J(4R z4M2K6uA^~IP*dMw?K!?V5i{AzrouM&l((CF|b6oD9L9Y6deIVK_LHJDA!w z9_EB8YNiy9O2R3PClY!cVz5_yfK4M6rjek8r3Yc%VlF|I!cPq?11qq!2nAu#-}F=d zfb;t1ImQd|W~uACr{%INZC&w17@}Fdd8pHpInjF#dJ!ThF-{DsSpNkWMnttE*1~wv zj^3AqL61nU#eZFHyQnm04r!JWLEReZKkMkw0FeXJYs1E}s7TVf2!9zG#~O+B7`US4 zNMTizR^IY6)f}w1W}0se1+tMQ-5gooJ!81T#-3SVczv>kbq8O^MjR2PUdw5ce#jg< z6ANB<7wMX*G`m|75y#4A4O5Cg#Im>t*-1VUkla^QO|sZl>0J-$KELU>5}BI`5tx>v z9zWl728}38WxBSJm*(fOMQsqdX@}oh&Qh=b;?DUUT{7HXUJEtZdO9@{(s)e2N<=(* z85xfriHkcG75|n~=2@km9d=1>2liqhz<5SA-zUSd9jdPDc=LW2H=O#SHl}e$<)rs# z9^Yg3CI~>{B^%I%6I$rni()wnv=J;HyIt1bOR9Z1D2rCAECZ^2Zq*SVeJa^(B@prM z!L=b$FDlN8Rsz942e)Y>>t!>DR9E3qTLj%AWmKOj!kjl#L|QrHcFJmO1xH`@C=g3y zfRS0Q|9~L*9M*8$9Ur3`n%QcA7LQ{=p0b7_B71bU+bSooPEw$5mYP8|FbGQdt)^D{ zE73Z0pm2n+<5Zmeo?m%Y*PAGn9CBA>h<*f3DyOi}AT8&brG*&_)|j@fu2#wOh$aq& z_9=p0>62`0Q3O~v6G7xAc~AyyA4jFQ8G3Kcx76E?^VRO?W~uu?&E0P6mz8VBu=5VN zZpd-+MkpXP92gg{4mSqfT2;YEvGzjjt<7=J!w^?3$V|b~C3pGs9B2NFArS?rc5SnZ ze~cHyp7%>X#n_XZshc&nQXrcE1P$4*Il-6}Yt~O>`+%(oHZe(Xqb}XxoG+2sOP>;d z15}hOFTsUwPihs)M(`nUjXmjgX|W?6tW?q+sgIMGWy9b`%KXaiDwYq`*b$S}KA*uu zv1g*PhyvN8RDDoWI4jaC>V&d4tEy_G1^FZIvsx3BKrVvb+(>GnMr04D!g}YEcv8wz zZRWqxlDKh*&~T9OaZ1%{X?#rZU&nq)iDpO^1c=sOAKStiLor6-4gCnilqLOErq^r- z{+!}oChrXqL0N0Rv^^B8%>T6-)y!W=8m{7?A%rN;g_kO7&9k7J@V+q0(LOi~3Pv1_ zwU%Bp>H)`Fm1uC>9Lt97xQ;H_;3pEiAezG=M!*k9Vex>y2x2S>7g9mbJb+Wz<_kLr zS!e{X=(zZySum~dOqqN(GGRnvC5Ug@m)vVSM)N@$Mty23Oq0SswkrBqcRzGw*dIy7 zk@>|MJik~#<}7RUp}xY>Qk({oR4o*#JD$}E5{8=K%y~NXl-^Pq!+63!0lpeW+orI! z8Xe>6K{6|F$n7IV1c;5?oL(Szp(6LbiZGj?zE_w8Q(DQXBYeX2L2b6webG2nU7L+& z?P@#R3?}w30msa-d~#@~19LQednC%bDl8K_c9G8BZG!kl0KKya`z2eOLR9KbSfidG6t1VHLx#@WcMw!P?sGi&Bm(I(ARN+ zLEh?dOn`BEqlVk%HlxVukn<~xwSNAl{-E|iscFy&D;ARiKjAOks*GO)BckJvGX$B5 z9*IY~p3uS4))c>_;4zOQz%7w!$dHY^hdo$QEOk28?>@FiK0CuJRv-qer$wxhYnO9xr7hv zz&$3&Q}Fl^x9la6pK9+Y z^@~X~)YEyEu7NyFMxrX`G$%FBPajV38+K6UyyL#gEsO70hmJ>u@0tMSYS{(-8(CzAt2_&<#hfP#vBaL1Qb;(sZ@&CD3?7iq z!W+dcaHEWKDu@G?cTF6XGv{`&rs#;eHYxX9R(|eOEb;d{+sHf@Z#g@cju;E{2TFyN z*ox3f@o-Ot`oG4B2e{}Ta=J;mFx#;`NVpwn;H^o){WKJV21m`)0zL-{CDr$Y2gy0&`kU;|lQg7o1;FJV+A`JIADP+KQVPXOOC+ z(HI+u(<7f=$!xG4<%qnJ7|;zXnaXPj)CN8SEE5KiGK}G~MT0GMV?4AM2_+tHXnk?I zqYz9vXyjr-53uWK`jcrT_YKm%g%=P!y8Bm=Ob{fJO|rS4qm%^#p&$68VB!%XDO-t# z&DSx}B;*=T2C1JY@vxx@f@Y`&*Y3dguFKq^p z@hL8%4r6nLf{_gkXh1=Ff>Z1ISd;5_+cz@kc#zqUGxn2YHQr1`^B7SC2Ds|zYff{E zuS+u_CztuLxbXq77H~cjBWPp51$%j4$nv-=!MfoQd-LqF0!q!K4Zb8mZZx=J?-nBb zVxkc6fv7Qf!fZBMkV(nzYf2wPeV5u{=)gG`z><0eq!pQ4g@TEc_jG$-gvuNm!FJ&x zQTZMw;V&+q!;uMFA=ZleWQu_cT6N%b9LlD->fUQoyluLqkOLwu)(mVNpXDz~=@p48H3UBu`T+^^mL%pB2PN ztly<&@{0>Ihu`znL-j?MfMqn1-ymm#y4>>)H_7VX;8-&JxpKLhE~ek4Ar4xp4PCwS z9WBEyH?0)oU;e-U30J?DLM*H6lPv!$@z5c*9*{^}{Tj7j~P6OnlKN{jHIp zB_1B>NU$L&i8+53b;gBhT}547RX1dj8oV0eKHY}6UatGF5lCHVSBPu=Gi z)tQ2*t^<}(#Sis@V5qv)B6-SSdxh(kS^D~R4)GU1E>1vb*lZC^yrDoTNZlJ9(jE5= z=Kk);=7LQHCFImfO1*D!C47%aPTwIa`XGF-FL#fFtaPOAb(JudKG{}rnQKLFxgqMy zoqNxZVR+Hnq`42sT3opzNQ6t`Nz2z%BcyR^@*@mC=OrKY+3*ZahMQr*DOvDJ&e0^* zJd+=|vsweFN|5A&JJixx$8EghRBXnF-F5_h{eQ(@^Qso;D;|a&CYo>)oPE=6w5s1! zZw#*b&9c=YPY8_!4jJKWpI=wd+xiqz2{XmYn^8;L;FF>)z1cOv+yhy|;pBS1+}3N) z%fofbdrO*uo+pa$E5I2{(2mu~GsU>!pmhNjYNS+x%~ZKEL+OjG7FRYfZbUsbF@l3_ zo-G#RSxelaxaZH!TvgFi@2OQy(OhtNS;DxMxGvw7{rzIItO)xKD=im(;WDXF4m50i z&TKjmmAoYsPKA4$>D7uhNE3z=WaZ)KVtvPcogFR?zNYPMzaxS-V>9%<^W}RtXVX5L zX`+>qUsbKf`n2adY83h3hM`8rK4EXI zev;Brzo)uW1E-P`mF(G?S?J##6N{9*tv>h4mb|mFVH38%s#-VbmR(}t;-I7>xxE?oe zIzW}0x_(5cLWh%1*x1Vl!E@F61$?h#ZelsRHDN{|GXrr192iRNV$~{wD40W;8(72A zBGBf^ZJ)mehLi+ga-hdQhx4u_i2AleX#{MJm0glls;@QCo`Exfw=RU)fZzJk{TxIi z3#pj$bu08##Ye8i*g4NPM)*L>>H;R;7V&cXGKS9^UGbMm#B|6nhD8`9=eR09v52Hn z`Orl^9Pb1}=5J_tl(2SYr&OIt-^Z0w+9ln{N0e&3@1!Ztc^$n445^_c%MV!t4BZ+X zm(~^g)=e+Vk@>SLdQQAMJuSb87js~!1X4mt=o44nYXfLRtQ2vfpf83S;Z@?&ekoQg zuC>XC-HKN?mHd&v=j`~Y2Zd%F7iZ~R^HDw57wb>sHsWzEgG(<+S|CGDlnh- z=a$7qc7zWT9g^1~N9ym(XrZ2d1NGFbB8Ya1np-Z*Lk9FyBzoh(nCQfcwZswOLWSdB zoMYcjv=^4yo4iBIJuVidCI>tA)smbodUh23fy8w$bmHiyT2j^3D`Z8(=sg6r$OxhB z?vR@!3)gk+t&(GhGr6Aj;ZU(Y447Wtl~b6lbJd!Vw3oUGE4>_cj#!G^m$Q!q{B8xh zS;OUS-kGZvdWFH2IFuA!LWL?VLa2=7az%8`C$48+#rCdCHE*FPuoucLWT`lU&NF3$ zPm`d0s-@zlIQP1QI_liDm~?Pd92yZ^Dhx){Y58)$IhufHh?I>f`;Z2<6u9fKHRiQ% zD6;vImciQE%LbolAoAq)hOMQ!Nn7bHr_yq)p$=f7vbH8QK(v(_vtLbRG3Y72v(YSk zdC-CWnb?U^U#Ej6IhcEIA2s{i>(~=TOf--aesEkS*ZxP2}(S8Nl_7Qk&f07kE~ygKu?tFDTPSiHj~`7du~-*(pcXTl~<1 z$#+Vu4rnZt@TSbT=WX4RD&>-1uejJ1xnfH+DK9!}!;D0KOj^S~)kj*=@L-*P;e<`y z$+NqPp@_}Ahd5sRgeNzdv-W)bT{teC%w7igcHZqJWXMMjeyVR>4e`Uv&?Sok9eRDs zmc8Yb+vWS{XxRlI0|{d?-vhbf_c|>tpvGIE-Lf^F-LP6SI*Tz!*=@4fK@13fvw}l(s_@V)j~zj6d!CeFpw}(6%x~2 zhf)_z_6WXrwpVgOLH&oBz9sTzr-StBQs8n6T(o)6wc3Jc4tZUk7E_h8o#9uHXka9q zXe9kq)$})Kgx>vg67b%w5wG<3PNA{D^maV7#7GP6>6R$2$a1a=f@43Eq%^F!#~g0+ z1?VW!&N}bc(aw{mel`iVlssCTs8SQ$YW*qMGDUs~q9xam+pwB3f@8ppcj?U9!vQnW zaahEEL zO+)*Z1e&nYKCrN9B#l7r~@<%g3Ft?)H|2axxVHD<;WRnPo7=Z_b(a5xNW?9d2XZwg z^R%j7KKehN=l#PzJ4XeDf5F?k{F8+bKA1g? z9hq5~SeWhXnEzeF#Z|)n1LU6u{U0@4)IO^Fm{mb84sOn-APIMny({IvLztQVOW)DW z+4gUD%uJag{$G3Mar;o;&0 zaPgRN1K2safdC#>GZO$e8<35IgBxVd$<6(5P%`!|uEzGJpueC#z?rN*a6oKq<{%z! zP5?U>h#kOg&IJOPZ~(ahEG%rC>})J-+@{=S{|2GtZ1oY9#wgPrfSg@EBJnRwRu(3}o7->e_a%n4T`WL#3gSo4Ru`@`-;)BNzu0F!^Z?4Gb{+=lMzd8Gtw1*|=uUYsY48Q^e z{DZK+IAi%IVa)$@nE9_Y{#OkAuY~^-UH_r$f5pK6O87s~_5T}P2>-pE0@;7u z1$lgImM{gA89z2!FeY+R;$ZK8y$gEEQ$AYY9i_Ehz`zhO|N4M~W#!;~G{U;dC`iB_ zLtvpIfTiU2gn@yPfys!AsClkU=UOFFjk#?HWTvSv9d9)A@x|ES;tq_643fbGvBL$I zSVkAoiNaD#WVC_4C0p^muA+odt&?JP1v^JJVHLvh*oFtEQzvrBdncyvy}R~kkqja8vSdIF5-vu6 zilFeTmAFMw@a~ngoGap{nX?2WujrJ2k=`!9a(#}oL>4V*Rjjw(NS`~!OEOsLz}YIPLq#iIqo|mw`Q`*0rig4tZ~yctx!-R znfB89+Tp~0qg!y!;WS@{RnKp9<1_!}(}n}W3DScV>jyin;N zi>g}f6tSE&J$BpX_?T|`$if1HsI0VRBppvTNJ#8^q+XJ4+Nf6%na&@B6wLU0{v;C2 z&JzNCJ5kH;kO3UhAi!{z_>qRE+!sc0{X%QKq-JlVFK4X3^GPP>`7|@B?IfG&J6}j5 zJl={wp%03}Iod}2A(JN(9m-+G6#4R+tJ0>#8s=IGrw_y6+I1jFrTy)egmPbXu!`CD zhK&cr_M&U{1;fML|b)AXTX-ZDOTW{HZ7wgL*iXq>4Y(5jn863t;Bf>RdIfTw@DzEKVg*E5!u|?6rP{i%U?hCa5fEE2QiO^c_O{Q~(gUozHQ9USHaUwtp9}*wX$R%{*(a8O=1!P2K-HPlo zhTy;(#%3CwBUf{ctlW{%j&EL?VED9E71oNqdzRMo4GpAXp-oV0$FGzeDr6?uyO?Wa zXXmCncN-CJbTJo!F@iu3mxPaWh~1EeQHLo<5jLhUPbD%Oo~(ovNhC3IA8VzT7#KZx z?W(|Pq2s*T3XltNz4k?{9heZ%7O7q`H#Kec57XPbKW7z8WRlQ6$4zGWnpGuK7H*(s zvzu}K66prQkX|OL*H#f7K9nJz$KMy%@r5sn$CA+S%wwoUL*tI0bnJ*uy|qI(D`z@$ zM=Yip4kJL>C$K5AhF0O*rR*w~t%-gmT@p|S)!Mve)y9TZS<2VYXBS!=Pw@M2*To23 zBeJQl(4A&^`W*>c>T{3aH(Rq)B;2j6La%{QAi8Rrh8M@A6@`a%gbHEz!bvn#P*mYH zP9XRv7FF3yME9Yoq{)0a!n5|xh!$iJ`O{T$SJH&1rV)uo;e^ChERHnlG;+y-T^FPc zkCbE0&i|?Dhrc2{=5H2qJQdI1ofp}#S9hPk_s6(&BHvUj;4Ed&n-X)YM3be{)<5R4 zN{ZDARw;h%1iIBG&jt?nI#9OE6k-NS77sY1QSC99er43rNj^W6S)s4Vo~osb5SvnN zszAfm4=$K8OImn1-l3=%Xkb`x#xDHCTT-Q_pe25Qr#0}aIA?O)!=coLjt0O6CYzo) zgQiWbHr4msltK^IMvKgYNYN)Ohc@m?ix@1+e4^!Y~aAoCm-9n}5$?0J9w@%eB zwr{pT*=2y*@c?I!nyfC#Sc`^^zA;Q4CRuRhF#t#`=Qx3;}l=6Az~5 z)!#|#`^IvD7QVq#Do>2wiARx%_J6rVhRbZqcSD|Gh+6#|n~5-U0e zyf9YUkAl*2>3XeCn}|@x4Tn&&nNUS7%bgWGz9%4(KU-oyx7+it1aFB2|1>JG2=&nD;qeqhH?&9v|PVZq0h>vH>6FEj)w0h z!aw&2MxgY?swO{e-fcJ?U*rh-gp=~$QRN*Qf6A6D9$9+%-Q3Bl6u@$DpSj3?Gi-Vf z3&}V+L8)DL-gLF*=er*SYstfzo;qxt!)!x8j()-&?Z_yDhwS;hgSp|lb0i)St65Ww zAVf;ZKw(PdT`tw*q~ckRA<*<(^(0$F^~V*tSEJ9Q6wHb~cn-p(qyf=I4S|!33muLT zHlg zoQ(!0(!X#Eq>U#&ezM3nK794E<@?qU9Vxq~Y!+yh)kz;Ftdktxq*t;-89N!qu8!V`F)u}5Quc94?@J$3dFdGGPf zBW8lrU6AhMj^D%0_s>eVokEZL;n_XIp`0#>ao9Mc=+iS$#wV0}q2xu}l9qAQX8sAr zXd2Eo(O*IIMXx@k4TLxmHGZ)tVfxqgi6z*2mzbvvNd9 zv3X{o8aRb>FfyLYez0!LzuxBcbf{^`q3{2klD`*M>~NjkcSGuRSxM`t&m|K*N6GC4 zPGfD^s{1;~{Du&4V-X;G06Xp~Us7yVvn+WkB^y0;6HCr{msTn%89a`mr4njs_@=MV zt)V+6c*a_GgQ+Pc?=m&s*p(sm?PuNC^wf=0%M|U=pU6y&keD|?+HB0P?^_9`tJ73r zmK{^-nwLi*5B`zN{x{4|%|cJ6zYPP3+k0Nzdw=yqR-gyR9({sU85bb^12>7w?EDyp zD;Yy{|GUg_rpBtQFD;st<61l@3cnYk2+3)ZHSq~<%D|N85Yw=IAv6jjVW=g#2~IZ8 zYbhF%9?PMQp-XYtw_Cwtd&&2)cm340EOBtq-4=z^KWzJXdVA-Tmb857f`yesqI?mC zJt_0VT+J91B|^{;dfU_o=CObUF8#^5Fg2(imQDYkq8w4yU21@k39y__WKi zHVVr2x3`|h5MRetFX~;LM_-HpV$879)a@-0m&st~du2)vk4u&&YTw{_{Z~<}P`c7B zf4U#y5Vt?x^TvKHH`eExYpE?>+B7|kN#az-jU=2DI(1Dy>e;}BSAR9V?JM5ndh~eT zUGY2)f+P~~PF*qd%jGf%i2F>Nu#X%eFE8Kt{!I8-ng48M-*rlJbaXW4e6Qn>nUM&F<+Fd0r08PrIJTzSq5lNWV82uKGroB4P&J#vXv&*RIe1x zWH!b7)i*{?J^-D=tIH$;enE0WA~mI^+T6|VfTc8%)Vhep$(Ao}@%2_IAYaMv%yh$Z z2zhE2P+%HI`e>P3(3kntwKRk(@I0izKRZO|4J9ItCGprhJ30ze-}jrqVm8l|8xO;x z!|&`g;-l+1GzgVsJLU@W{1L_S(4FJs00ULiNU^zd^0>@gOmkz7XB>~d!>2RlfZW{TQY>cJRxcdye^@;eW_u+i ztTl-rG?|q=zH{}`5Gy;}xH(lME|@$FV*k|s8jXu8Fk8I+P7D!%>A+Gl@7PEjQTdI& zth`)n!w0%ngK!duJzIfy6J^sLS9cYvR|zdtCQSnxFLs}s9-<<$lRG|TqJd5}L|?|+ z1}(fJen_tVD~gC-mU&pqA7lC7Al82OeQd~j7J!5IaW{N0B>!Xkd&JfngOq_6Pt-;! znMux`YPu@jdLbXrH-+|%!Og4g?2iJ@z!o!J$!7I8EpTp{*Je8Nn2nW|h9H`T8hT2MG!%f#RFxdYQwHPWADXJBaK=eAKqhflWU5bZ< z`BfTkl{O{ib4^_hkdJNh`*-$zUW1S9QFl9#2|I(Ako0RupMt7yt{+&uy$E#+D5JgL$cjV); zn&sF?SFHFXM{ec8yWxFqxAChtV5$=lm)qxzsyv=}|3-?`xLIiDth!!Q8z)_LSVbH! z94aV6hzNM{fR6(Em?wn?RY{Ik>?V&zYg!5>r<2puFr4j>TUCSRbgEawoIN>0je@S5 zHQ9<&q>R*4gd?a+-t2Oh!%dK0ZFFbb!CqMV2&>xAVUaZJ|9kuPc9Qv~Jpa9(&g$l( zJ|ONm-#?2Gm1yUg(0)hYsuMD(Z2NqEd&6k%Y8O_}3G28A%#{f9SMN!j;c1 zG!x?^|7W36d81pFhLdu5kRBT=e5JS-IKe+gF(3## z9&dOqcXz&rlZJaE<)}?5x``{4`z1xockB@IQ~EvrGSGP<2J_pE~gdoHDV)gp&A4{!+4PJ%9cSE|?{g zj$Cr>^I>LWvT9I8{KplX*d!wxUX=M>^)F3!{XhdE0u-8wWC=IRmZcxFI{9NxS8 zm6TjC8LP0YfFhLE1bOV>pe%#t?U&oqi3fc}na+2M02DcG{_E2BI8#0u!wlDqCIH3k z@#D>+mTH5maTrT{L#f126Wm*1TpDuCJw}__zQ(xars!_HW7PgwZPvu&M)W$)SI*u5 z!X6r^JmyHZFfz-oltpo4_T>`XV*1$Dt2B_)``ZB; z*hOXWN){nT>HU=%h==6)``Z{|j4Z94h)#N%hgLNH${yewIcG!7#X;YesPEU@8*iT* zSD9zJ?EV8E%wVj;dHg@^7B^M%X07N(I<`K{tGerrPa}qp)G#67QU?AcAS~#SF0y!O z37w;Pi&Z-9$v9#oZ3kv1=hLkKb)^%mL5@F=CvdKjiQp3m%D`a^@HQkBjZjtA&lef1 z`Xmbsy4r9jimrD*hEdO1S$V!$2;R*;rOdvgSL4neiGTdBMrVgB!5F#6Jxi;&CM7{9 z_x}W&7i8!q8r;ka0qg|wzncEjj|m&t8~$nHyI z9e<3Dt0iGo7Rl8XfLS+Iv}Sg8hR^)7U*^C4@gJdMAJV{7E}eMo7B>jtk1sxJ^yFt?9I3 zqN|ynfMz2ty0k2mrLZi8QVvSBv27crY-F0_I?ksQh%NO-oroFf%rN3B3ZoQ2rz^w) z0;319Fj~XrsUCSDPPtkwLm`UBOj(aDp_9QkAGn$K-Sc4%KJi_YvWR0%v)Lew2Y7yx zTO5XA?zhBR6Z$^t_y6@XxUQ8y8?KGx+PIF5tt_Oauv80W36v$UWSTf8%m`VUvWYMv zXJ*285uVMGm$#bu@u~Y_`vi<_kEdz>%BI&_O1q+({jOv#zNAsYmL%e3C2DDN6r=M` z-nmp}h>_$YANeSM_xK|;Z3}5>gtTxRn>dac48z>0P)g;*Q6K)$P362IJ*osQcY?%d zr{xjzv&zZa>U}*5kq9Ab{RdOTW%n~2dJMe*gF&CuZyn?2TW(2$PC6^FF$!=}U6E%s z3sKXXSmaG*L`4@*O-*tC{h#Lle&vhw`U6zD8j{XLE^!*U+xZEd*Ce9huDkE%)mIO5@adF$3ZEDs&ed)VuFQ{I+F2K9LH&_K<9DmAP9)UB!-ZcO`kt^ zmSbzz+$+L=;3}koMAziK3V&3ORrNEJt5|onyz2a`wzwE}Xx>;^HFRi7u^X zlXj=g#KaU+lhf?pwUHoWEM?JWrnu*MXsxkrJJBVHeQ&aR(GGmj?{oUpan77M&GOPkdc948 zz-MD)o%OXK6y?&CN;aId=4FsAukDsO6oub|D((X*KYxiCjx>qI1U}-6r*1F_W zib}f<&l_;#^?R9|?2IjV%<-Kq@uiBW6x(pO*y2k@yune-DvwX?6ko0NtTcnckW;5l zjy*lMwz}O(uDD_^Q2JQo(u#-MXb?pqzBfn{!2FVFne=am za_63cIKD4<*Sl^>2`9=;gj3I|w1EpncU0aEgX(dx|IUHt9bbFQKq)QWdh;lU4J#^-=#nmArj*n{5b(`!eVduNt8?Ym?oQyiPEC_c!z8WQF=5c(+j2aQ5_I2SW8CzP6r1FmjkutD=u83WBePs{Ffo+urUut2rNmNs_A$v!Lr zk^L1m$iX4C4ewiNZD#L zN^@de=#Oe+EZ=e*H#fia_rbKe(-~*IQ;6N z9d9HM#}S|Wsryl35GLNSPC0@kYx~5>lT1wSO@TNI+i_`ky1BhvE3!&Oo+6zL!G-f@ zxckl<+1T7f$Avf;ZL?Pq3ni1a2HiCYASF6$vM)v6MY?FFlK_7*>-RYJS@9tZBSc8a zpx0wC81Udj-{R#%FL3PGF|N7#8jiksG;bMYNx7qZw9G=Co}T6Be)iLR;GTO?VHhTp zloBEnR|kLabYd5)1Q6?Vy0lvDydjtvX+2cbikDImq+or01;=q%URo|b-=={JX$^Qb zQE?p8>uu8O_38I}c%DxfMhu1?p63(zNezdTlHI%J*t>U;g@rvOq`CUcM4U*YiCO)) zNZZRMu#!MolJgfX^1_QR5^0+ceDI^(`{9ps-Ss!}SO4n^oH%(BrR;4nKOM&@c$px) zi+dKi<2`rs{`cL({H}Ro9iyT!N@Ko-$cp2b$DeqXn{T}%Ed|!>+O>e~*o?Ap^|dZB z_F&LsW$6t4exH7?ho)%0lHpfIMt*KpU0hn??AZ&fu5J=)3)^;RHd{yDm64`c408c;t45&%ockd@zT$pESqCu}W%VUo`%vD!ko2>k_L`_+WW~;^Q z>@FJ37L7)SlP8bUZra>-+iht*$|U>uUrW2y#&(=^!+|8VRUABcP-l~qRRw+f+u!D; z!zY-ZUm%WS9Jj$cZo0MDi*IZmRf2s=qf^21$`TKK?F;PRzn}H>4FW$%>a&%L-zNwHf*_>d-{i0V`pfLU@ebD4RzV2%EMCFF!roLY_yj?+2b7MJ?7t|C z(lBMn`q~Q1%gYP~eUxRId^tol$HwGMh^7|Zn>nK+&&eH8PX|s_k0yalzW3d4@}YZfXK`_H zBmq2&6^REPct8h%kLLvhfzQ&#i#+rEYdCI;wY3#CH`h~T?j()uk#xFKOwH`VwwW78Xi7zdiQY(wrCDPj9VpZ*|VZN10y&ppHWbEoKStTHpzWNK=PbLY>qw9@Cg8{S1{Vj9IGCSR6ZuWyLt@Mb(kinqObI(4(%IZ3nj#xSW3P+Fh2_l>B{$(^Gr{5*u8s!_rCYeqP>&MdU~a$ch9}}ny%X| zdQ>#jZGs}kOn!C$-0?{XCB0DHC9SfviWnJhPPQ0nTv^~;Ze z)mJhzGex)CWny9?>C>2IE9SeLW`0nH?>+azYq~c3yKa(45)R7pN!08WTivxM09qFd9`L3T76Cn(dvBfh>#*^vBUY2Ds91Qs8*B{{U zp{M!SM?XTh+f9_brLbhaIbM;-mu(xH_0&qed0Fv?RGe>stZPLN+Snm_gis@0GV?%H z6lwe*%(*ZOBjQ+>A|2Jhk=YfyfbtElH6a>N-mIkxe-ut$wvB~_P@Tpm@r$}@^JVHn{BA-)$RRY_?~UB^1VXf`ROn<-70to+}GV@HT3(-u-P z$Io)^ljilxsihfG5co;nZQy%kK3N!rM&+y?2?apXU|@57nNGKho1y@j$t;DeT)U{3 zAJH4eQ=L4$#Ohj~>8TFWlbw>xHFu@V_uJF3+4C3-J$ye(f-aiU#U0y9b8ktlhs?Ya zIr>_(&JzZwl5=d%!Bv7VUd$A`Pm44OEl4Ie3<<-4FbMNwQR>KMrOI`vZ)W8=j!U!I zq|s>LxDD3VRylX}43B^Rd$e0ECMPEohL92|Gpp3FoMeWVl}+5LNEAY#E?zhTdN*;b z+30!9%}mg0I_b_d<*uXsfzQf%FW*#BruEt5I5*?i>8Bp!{Mq9u zOVI6h*}Hcy*X`TKyAS+R9LE{a72{U4`B~j-C8c0x`8@r8gUQKhY};Wl3}`kTCOWNr z#jLLP84d$%<>bd|Ie9DxZ!MCl@?1(u7zPZ7LwtXT@A>%_-BBE&qnJp?`PSXpq}OtK zN(kF_Xtz6bCnjmN+N`WB^Xj1&dF$Al3vC)r1_vVzHaEy|KU%kEn_ww+=|F{`Ufc;0~7xm_S6QPij1ZlpGO zlGIg#ucaY=GWdFA!pk?6SUYKkqK=89q^m(DV!WY;?+^2Qr8?PN<@#iUdxh;dN#~v* zpx5gWh5<@hX?u$ZZ_ww&Td#59{AmXL4P3`&&z?o@ec$aYEG!^^C>2%aBuaxxaw9@k z4TQpKs#_tVGVxHyHHRu$9DhUAmp5;C^OmlIyRzg_)TdY}?LrzqBszB$}7D8Ix;9J*5<- zu&*>$LZJ_2sf*nR)|vW2`G&{3*r8er15GViv1wVu@BZHBbl;1)@ur*EwR;as7tbf% zYYatXNlClY#j<4TPejB~oVOsVZcJR80GIxkQsTM|n#~r?Ruji@xp?scr%#>W+}Se- z9iwcO#O~u5Woc$-rdV8DOpAUL?M@rVc8a||w65&BQ)IPBDT_UzWag~N9AC;H_bhsd zs9*7yU5i-M_LN-TaXEJ4=+UD(3?m+T=s}iO``q>Z53#wqiR(JKd02`|S6mfSnI@c^ zLGBXY_wl?TknI1|Y_)J5m-Y2Ej=b_B=g*$NbtLU}hrN6Ea^pL0q}%D%CQKAuJ6dN_ zg|X|76&sS7#s{^|X}*VAX|F0O@flrO4O#R~lu^@CYF?3=tqUholtEN+^5jXKC`QTJ z+8WC%s~mal4PHBXl8^u7{V3b!+}YD5F3)~wqtT?@?$GIWX*8NFFJ0u&p%*!E{4Iuq z0j{f9T-e36`>#&2yp@#_EGbxNx7+DH97$oMj?E3q(#bX&8&~CXhM=0^)~%B8E|ID0 z(@hMH*Cu4A4emE#e`9Ydk#3)4?l>th_f*%23l}bwaB-cs4H^sveC=yrCB|l9@3kn~ z0d%rIhBqLNLc-9ezqwAozfRx{X*OK0xZ(9GAv8Mw9y@4>i0Sx(;dl3 zH4+;e8`{iPta8?HFrYse@W>;NvUl$y`}gfjAZMj;?5wa;p_G+p{#7ep-!8{qQhDrl zdDME`pwSIgwGfnghl`@UQjI2030?e07yB$i7U$80p_9%#=57+beotqcU1`dD^$A^Y zl_jm*R5fqDv|NSA&%xwQQDC`6P^lcL(nbO02=~@-nXU%5s5d6YZq;2Sk{HL^Z?hI* z`|R&BY5V=Y-Z7_%`p~Qp<@{M)Cxy*$Y?hMAXn-l>A`Gjfv`J~Utx>Kxb$L68OahfK zWl@Ng>e3Nr`lvx8ZET3FOWCAGxwNRs1W4c4^h6Qc5k`aP_XipSeizl9Dvi}+F0JDC zi3)QW=7`v)@r*Iv_Cwmx+y9 z!)!3pDNKS_DuT-EMAz4* z)X{BB%IoFOTnq*Sohw>xnsVjFUAhw2t4-Ib+}NlfsIMFoTm7bg7>xU)Tp^;wg~FJX zA`YTc4wuHtsnqRd%qfapF|$qxqLL3bx%8Ej33eC{{fxU Vtc#mjbM^oL002ovPDHLkV1f;8thWFF literal 0 HcmV?d00001 -- 2.45.1 From 40c776c9a78469b1728a03eb8aa3ac914e69acfb Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 28 Dec 2023 09:26:53 -0500 Subject: [PATCH 21/26] Add repo README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..109625d --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# BitBadger.Documents + +This library provides a lightweight document storage implementation backed by either PostgreSQL or SQLite. Both of these databases have great support for storing, retrieving, and manipulating JSON fields; this library leverages that, and provides a straightforward way to store documents. + +## NuGet Packages +| PostgreSQL | SQLite | +|------------|--------| +|[![Nuget](https://img.shields.io/nuget/v/BitBadger.Documents.Postgres?style=plastic)](https://www.nuget.org/packages/BitBadger.Documents.Postgres/)|[![Nuget](https://img.shields.io/nuget/v/BitBadger.Documents.Sqlite?style=plastic)](https://www.nuget.org/packages/BitBadger.Documents.Sqlite/)| + +## More Information + +See [the project site](https://bitbadger.solutions/open-source/relational-documents/) for a full description and documentation. -- 2.45.1 From d9e37445a8a9799adb234b03e9a3eb6f45c5dd26 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 28 Dec 2023 19:39:02 -0500 Subject: [PATCH 22/26] Change "update partial" to "patch" for Postgres --- src/Common/Library.fs | 13 +- src/Postgres/Extensions.fs | 72 ++++----- src/Postgres/Library.fs | 149 +++++++++--------- src/Sqlite/Library.fs | 2 +- src/Tests.CSharp/CommonCSharpTests.cs | 10 +- .../PostgresCSharpExtensionTests.cs | 40 +++-- src/Tests.CSharp/PostgresCSharpTests.cs | 63 ++++---- src/Tests/CommonTests.fs | 14 +- src/Tests/PostgresExtensionTests.fs | 36 ++--- src/Tests/PostgresTests.fs | 56 +++---- 10 files changed, 225 insertions(+), 230 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 0c85cce..2f45c2b 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -163,6 +163,11 @@ module Query = "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data" tableName (Configuration.idField ()) + /// Query to update a document + [] + let update tableName = + $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" + /// Queries for counting documents module Count = @@ -202,14 +207,6 @@ module Query = let byField tableName fieldName op = $"""{selectFromTable tableName} WHERE {whereByField fieldName op "@field"}""" - /// Queries to update documents - module Update = - - /// Query to update a document - [] - let full tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Queries to delete documents module Delete = diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index e1bd553..4602341 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -109,29 +109,29 @@ module Extensions = member conn.findFirstByJsonPath<'TDoc> tableName jsonPath = WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (Sql.existingConnection conn) - /// Update a full document - member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = - WithProps.Update.full tableName docId document (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) - /// Update a full document - member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = - WithProps.Update.fullFunc tableName idFunc document (Sql.existingConnection conn) + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithProps.Update.byFunc tableName idFunc document (Sql.existingConnection conn) - /// Update a partial document - member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPartial) = - WithProps.Update.partialById tableName docId partial (Sql.existingConnection conn) + /// Patch a document by its ID + member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) = + WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn) - /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) - member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPartial) = - WithProps.Update.partialByField tableName fieldName op value partial (Sql.existingConnection conn) + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + member conn.patchByField tableName fieldName op (value: obj) (patch: 'TPatch) = + WithProps.Patch.byField tableName fieldName op value patch (Sql.existingConnection conn) - /// Update partial documents using a JSON containment query in the WHERE clause (@>) - member conn.updatePartialByContains tableName (criteria: 'TCriteria) (partial: 'TPartial) = - WithProps.Update.partialByContains tableName criteria partial (Sql.existingConnection conn) + /// Patch documents using a JSON containment query in the WHERE clause (@>) + member conn.patchByContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = + WithProps.Patch.byContains tableName criteria patch (Sql.existingConnection conn) - /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - member conn.updatePartialByJsonPath tableName jsonPath (partial: 'TPartial) = - WithProps.Update.partialByJsonPath tableName jsonPath partial (Sql.existingConnection conn) + /// Patch documents using a JSON Path match query in the WHERE clause (@?) + member conn.patchByJsonPath tableName jsonPath (patch: 'TPatch) = + WithProps.Patch.byJsonPath tableName jsonPath patch (Sql.existingConnection conn) /// Delete a document by its ID member conn.deleteById tableName (docId: 'TKey) = @@ -282,35 +282,35 @@ type NpgsqlConnectionCSharpExtensions = static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) - /// Update a full document + /// Update an entire document by its ID [] - static member inline UpdateFull(conn, tableName, docId: 'TKey, document: 'TDoc) = - WithProps.Update.full tableName docId document (Sql.existingConnection conn) + static member inline UpdateById(conn, tableName, docId: 'TKey, document: 'TDoc) = + WithProps.Update.byId tableName docId document (Sql.existingConnection conn) - /// Update a full document + /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] - static member inline UpdateFullFunc(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = - WithProps.Update.FullFunc(tableName, idFunc, document, Sql.existingConnection conn) + static member inline UpdateByFunc(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + WithProps.Update.ByFunc(tableName, idFunc, document, Sql.existingConnection conn) - /// Update a partial document + /// Patch a document by its ID [] - static member inline UpdatePartialById(conn, tableName, docId: 'TKey, partial: 'TPartial) = - WithProps.Update.partialById tableName docId partial (Sql.existingConnection conn) + static member inline PatchById(conn, tableName, docId: 'TKey, patch: 'TPatch) = + WithProps.Patch.byId tableName docId patch (Sql.existingConnection conn) - /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) [] - static member inline UpdatePartialByField(conn, tableName, fieldName, op, value: obj, partial: 'TPartial) = - WithProps.Update.partialByField tableName fieldName op value partial (Sql.existingConnection conn) + static member inline PatchByField(conn, tableName, fieldName, op, value: obj, patch: 'TPatch) = + WithProps.Patch.byField tableName fieldName op value patch (Sql.existingConnection conn) - /// Update partial documents using a JSON containment query in the WHERE clause (@>) + /// Patch documents using a JSON containment query in the WHERE clause (@>) [] - static member inline UpdatePartialByContains(conn, tableName, criteria: 'TCriteria, partial: 'TPartial) = - WithProps.Update.partialByContains tableName criteria partial (Sql.existingConnection conn) + static member inline PatchByContains(conn, tableName, criteria: 'TCriteria, patch: 'TPatch) = + WithProps.Patch.byContains tableName criteria patch (Sql.existingConnection conn) - /// Update partial documents using a JSON Path match query in the WHERE clause (@?) + /// Patch documents using a JSON Path match query in the WHERE clause (@?) [] - static member inline UpdatePartialByJsonPath(conn, tableName, jsonPath, partial: 'TPartial) = - WithProps.Update.partialByJsonPath tableName jsonPath partial (Sql.existingConnection conn) + static member inline PatchByJsonPath(conn, tableName, jsonPath, patch: 'TPatch) = + WithProps.Patch.byJsonPath tableName jsonPath patch (Sql.existingConnection conn) /// Delete a document by its ID [] diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 1ea570a..5fcb7cc 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -142,27 +142,27 @@ module Query = let byJsonPath tableName = $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" - /// Queries to update documents - module Update = + /// Queries to patch (partially update) documents + module Patch = - /// Query to update a document - [] - let partialById tableName = + /// Query to patch a document by its ID + [] + let byId tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}""" - /// Query to update a document - [] - let partialByField tableName fieldName op = + /// Query to patch documents match a JSON field comparison (->> =) + [] + let byField tableName fieldName op = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}""" - /// Query to update partial documents matching a JSON containment query (@>) - [] - let partialByContains tableName = + /// Query to patch documents matching a JSON containment query (@>) + [] + let byContains tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}""" - /// Query to update partial documents matching a JSON containment query (@>) - [] - let partialByJsonPath tableName = + /// Query to patch documents matching a JSON containment query (@>) + [] + let byJsonPath tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}""" /// Queries to delete documents @@ -436,48 +436,46 @@ module WithProps = [] module Update = - /// Update an entire document - [] - let full tableName (docId: 'TKey) (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.Update.full tableName) [ idParam docId; jsonParam "@data" document ] sqlProps + /// 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 - /// Update an entire document - [] - let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps = - full tableName (idFunc document) document sqlProps + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + [] + let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps = + byId tableName (idFunc document) document sqlProps - /// Update an entire document - let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) = - fullFunc tableName idFunc.Invoke document sqlProps + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) = + byFunc tableName idFunc.Invoke document sqlProps + + /// Commands to patch (partially update) documents + [] + module Patch = - /// Update a partial document - [] - let partialById tableName (docId: 'TKey) (partial: 'TPartial) sqlProps = - Custom.nonQuery (Query.Update.partialById tableName) [ idParam docId; jsonParam "@data" partial ] sqlProps + /// 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 - /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) - [] - let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) sqlProps = + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let byField tableName fieldName op (value: obj) (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Update.partialByField tableName fieldName op) - [ jsonParam "@data" partial; fieldParam value ] - sqlProps + (Query.Patch.byField tableName fieldName op) [ jsonParam "@data" patch; fieldParam value ] sqlProps - /// Update partial documents using a JSON containment query in the WHERE clause (@>) - [] - let partialByContains tableName (criteria: 'TContains) (partial: 'TPartial) sqlProps = + /// Patch documents using a JSON containment query in the WHERE clause (@>) + [] + let byContains tableName (criteria: 'TContains) (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Update.partialByContains tableName) - [ jsonParam "@data" partial; jsonParam "@criteria" criteria ] - sqlProps + (Query.Patch.byContains tableName) [ jsonParam "@data" patch; jsonParam "@criteria" criteria ] sqlProps - /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - [] - let partialByJsonPath tableName jsonPath (partial: 'TPartial) sqlProps = + /// Patch documents using a JSON Path match query in the WHERE clause (@?) + [] + let byJsonPath tableName jsonPath (patch: 'TPatch) sqlProps = Custom.nonQuery - (Query.Update.partialByJsonPath tableName) - [ jsonParam "@data" partial; "@path", Sql.string jsonPath ] - sqlProps + (Query.Patch.byJsonPath tableName) [ jsonParam "@data" patch; "@path", Sql.string jsonPath ] sqlProps /// Commands to delete documents [] @@ -707,39 +705,44 @@ module Find = [] module Update = - /// Update a full document - [] - let full tableName (docId: 'TKey) (document: 'TDoc) = - WithProps.Update.full tableName docId document (fromDataSource ()) + /// Update an entire document by its ID + [] + let byId tableName (docId: 'TKey) (document: 'TDoc) = + WithProps.Update.byId tableName docId document (fromDataSource ()) - /// Update a full document + /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] - let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = - WithProps.Update.fullFunc tableName idFunc document (fromDataSource ()) + let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithProps.Update.byFunc tableName idFunc document (fromDataSource ()) + + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + WithProps.Update.ByFunc(tableName, idFunc, document, fromDataSource ()) - /// Update a full document - let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = - WithProps.Update.FullFunc(tableName, idFunc, document, fromDataSource ()) - /// Update a partial document - [] - let partialById tableName (docId: 'TKey) (partial: 'TPartial) = - WithProps.Update.partialById tableName docId partial (fromDataSource ()) +/// Commands to patch (partially update) documents +[] +module Patch = - /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) - [] - let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) = - WithProps.Update.partialByField tableName fieldName op value partial (fromDataSource ()) + /// Patch a document by its ID + [] + let byId tableName (docId: 'TKey) (patch: 'TPatch) = + WithProps.Patch.byId tableName docId patch (fromDataSource ()) - /// Update partial documents using a JSON containment query in the WHERE clause (@>) - [] - let partialByContains tableName (criteria: 'TCriteria) (partial: 'TPartial) = - WithProps.Update.partialByContains tableName criteria partial (fromDataSource ()) + /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let byField tableName fieldName op (value: obj) (patch: 'TPatch) = + WithProps.Patch.byField tableName fieldName op value patch (fromDataSource ()) - /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - [] - let partialByJsonPath tableName jsonPath (partial: 'TPartial) = - WithProps.Update.partialByJsonPath tableName jsonPath partial (fromDataSource ()) + /// Patch documents using a JSON containment query in the WHERE clause (@>) + [] + let byContains tableName (criteria: 'TCriteria) (patch: 'TPatch) = + WithProps.Patch.byContains tableName criteria patch (fromDataSource ()) + + /// Patch documents using a JSON Path match query in the WHERE clause (@?) + [] + let byJsonPath tableName jsonPath (patch: 'TPatch) = + WithProps.Patch.byJsonPath tableName jsonPath patch (fromDataSource ()) /// Commands to delete documents diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 21e82f2..9920d15 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -298,7 +298,7 @@ module WithConn = /// Update an entire document [] let full tableName (docId: 'TKey) (document: 'TDoc) conn = - Custom.nonQuery (Query.Update.full tableName) [ idParam docId; jsonParam "@data" document ] conn + Custom.nonQuery (Query.update tableName) [ idParam docId; jsonParam "@data" document ] conn /// Update an entire document [] diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index ecfb9f9..c75d2ba 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -172,6 +172,11 @@ public static class CommonCSharpTests $"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); }), + TestCase("Update succeeds", () => + { + Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", + "UPDATE full statement not correct"); + }), TestList("Count", new[] { TestCase("All succeeds", () => @@ -214,11 +219,6 @@ public static class CommonCSharpTests "SELECT by JSON comparison query not correct"); }) }), - TestCase("Update.Full succeeds", () => - { - Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", - "UPDATE full statement not correct"); - }), TestList("Delete", new[] { TestCase("ById succeeds", () => diff --git a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs index 103e890..4f0ee63 100644 --- a/src/Tests.CSharp/PostgresCSharpExtensionTests.cs +++ b/src/Tests.CSharp/PostgresCSharpExtensionTests.cs @@ -558,7 +558,7 @@ public class PostgresCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) }), - TestList("UpdateFull", new[] + TestList("UpdateById", new[] { TestCase("succeeds when a document is updated", async () => { @@ -566,7 +566,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdateFull(PostgresDb.TableName, "one", + await conn.UpdateById(PostgresDb.TableName, "one", new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }); var after = await conn.FindById(PostgresDb.TableName, "one"); Expect.isNotNull(after, "There should have been a document returned post-update"); @@ -585,11 +585,11 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdateFull(PostgresDb.TableName, "test", + await conn.UpdateById(PostgresDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - TestList("UpdateFullFunc", new[] + TestList("UpdateByFunc", new[] { TestCase("succeeds when a document is updated", async () => { @@ -597,7 +597,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdateFullFunc(PostgresDb.TableName, doc => doc.Id, + await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); var after = await conn.FindById(PostgresDb.TableName, "one"); Expect.isNotNull(after, "There should have been a document returned post-update"); @@ -614,11 +614,11 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdateFullFunc(PostgresDb.TableName, doc => doc.Id, + await conn.UpdateByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) }), - TestList("UpdatePartialById", new[] + TestList("PatchById", new[] { TestCase("succeeds when a document is updated", async () => { @@ -626,7 +626,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdatePartialById(PostgresDb.TableName, "one", new { NumValue = 44 }); + await conn.PatchById(PostgresDb.TableName, "one", new { NumValue = 44 }); var after = await conn.FindById(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"); @@ -639,10 +639,10 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialById(PostgresDb.TableName, "test", new { Foo = "green" }); + await conn.PatchById(PostgresDb.TableName, "test", new { Foo = "green" }); }) }), - TestList("UpdatePartialByField", new[] + TestList("PatchByField", new[] { TestCase("succeeds when a document is updated", async () => { @@ -650,7 +650,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdatePartialByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + await conn.PatchByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); var after = await conn.CountByField(PostgresDb.TableName, "NumValue", Op.EQ, "77"); Expect.equal(after, 2, "There should have been 2 documents returned"); }), @@ -662,11 +662,10 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); + await conn.PatchByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); }) }), - TestList("UpdatePartialByContains", new[] + TestList("PatchByContains", new[] { TestCase("succeeds when a document is updated", async () => { @@ -674,8 +673,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdatePartialByContains(PostgresDb.TableName, new { Value = "purple" }, - new { NumValue = 77 }); + await conn.PatchByContains(PostgresDb.TableName, new { Value = "purple" }, new { NumValue = 77 }); var after = await conn.CountByContains(PostgresDb.TableName, new { NumValue = 77 }); Expect.equal(after, 2, "There should have been 2 documents returned"); }), @@ -687,11 +685,10 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialByContains(PostgresDb.TableName, new { Value = "burgundy" }, - new { Foo = "green" }); + await conn.PatchByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); }) }), - TestList("UpdatePartialByJsonPath", new[] + TestList("PatchByJsonPath", new[] { TestCase("succeeds when a document is updated", async () => { @@ -699,8 +696,7 @@ public class PostgresCSharpExtensionTests await using var conn = MkConn(db); await LoadDocs(); - await conn.UpdatePartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", - new { NumValue = 1000 }); + await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", new { NumValue = 1000 }); var after = await conn.CountByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 999)"); Expect.equal(after, 2, "There should have been 2 documents returned"); }), @@ -712,7 +708,7 @@ public class PostgresCSharpExtensionTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + await conn.PatchByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); }) }), TestList("DeleteById", new[] diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 72af8cb..a4713c7 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -124,29 +124,29 @@ public class PostgresCSharpTests "SELECT by JSON Path match query not correct"); }) }), - TestList("Update", new[] + TestList("Patch", new[] { - TestCase("partialById succeeds", () => + TestCase("ById succeeds", () => { - Expect.equal(Postgres.Query.Update.PartialById(PostgresDb.TableName), + 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("partialByField succeeds", () => + TestCase("ByField succeeds", () => { - Expect.equal(Postgres.Query.Update.PartialByField(PostgresDb.TableName, "Snail", Op.LT), + Expect.equal(Postgres.Query.Patch.ByField(PostgresDb.TableName, "Snail", Op.LT), $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", "UPDATE partial by ID statement not correct"); }), - TestCase("partialByContains succeeds", () => + TestCase("ByContains succeeds", () => { - Expect.equal(Postgres.Query.Update.PartialByContains(PostgresDb.TableName), + 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("partialByJsonPath succeeds", () => + TestCase("ByJsonPath succeeds", () => { - Expect.equal(Postgres.Query.Update.PartialByJsonPath(PostgresDb.TableName), + 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"); }) @@ -722,14 +722,14 @@ public class PostgresCSharpTests }), TestList("Update", new[] { - TestList("Full", new[] + TestList("ById", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.Full(PostgresDb.TableName, "one", + 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"); @@ -748,18 +748,18 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.Full(PostgresDb.TableName, "test", + await Update.ById(PostgresDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - TestList("FullFunc", new[] + TestList("ByFunc", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.FullFunc(PostgresDb.TableName, doc => doc.Id, + 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"); @@ -776,18 +776,21 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.FullFunc(PostgresDb.TableName, doc => doc.Id, + await Update.ByFunc(PostgresDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) - }), - TestList("PartialById", new[] + }) + }), + TestList("Patch", new[] + { + TestList("ById", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.PartialById(PostgresDb.TableName, "one", new { NumValue = 44 }); + 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"); @@ -800,17 +803,17 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialById(PostgresDb.TableName, "test", new { Foo = "green" }); + await Patch.ById(PostgresDb.TableName, "test", new { Foo = "green" }); }) }), - TestList("PartialByField", new[] + TestList("ByField", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.PartialByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + await Patch.ByField(PostgresDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); var after = await Count.ByField(PostgresDb.TableName, "NumValue", Op.EQ, "77"); Expect.equal(after, 2, "There should have been 2 documents returned"); }), @@ -822,19 +825,17 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); + await Patch.ByField(PostgresDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); }) }), - TestList("PartialByContains", new[] + TestList("ByContains", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.PartialByContains(PostgresDb.TableName, new { Value = "purple" }, - new { NumValue = 77 }); + 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"); }), @@ -846,19 +847,17 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialByContains(PostgresDb.TableName, new { Value = "burgundy" }, - new { Foo = "green" }); + await Patch.ByContains(PostgresDb.TableName, new { Value = "burgundy" }, new { Foo = "green" }); }) }), - TestList("PartialByJsonPath", new[] + TestList("ByJsonPath", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = PostgresDb.BuildDb(); await LoadDocs(); - await Update.PartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ > 10)", - new { NumValue = 1000 }); + 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"); }), @@ -870,7 +869,7 @@ public class PostgresCSharpTests Expect.equal(before, 0, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); + await Patch.ByJsonPath(PostgresDb.TableName, "$.NumValue ? (@ < 0)", new { Foo = "green" }); }) }) }), diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index a0ce0d7..b7fcc4a 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -95,6 +95,12 @@ let all = $"INSERT INTO {tbl} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data" "INSERT ON CONFLICT UPDATE statement not correct" } + test "update succeeds" { + Expect.equal + (Query.update tbl) + $"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id" + "UPDATE full statement not correct" + } testList "Count" [ test "all succeeds" { Expect.equal (Query.Count.all tbl) $"SELECT COUNT(*) AS it FROM {tbl}" "Count query not correct" @@ -134,14 +140,6 @@ let all = "SELECT by JSON comparison query not correct" } ] - testList "Update" [ - test "full succeeds" { - Expect.equal - (Query.Update.full tbl) - $"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id" - "UPDATE full statement not correct" - } - ] testList "Delete" [ test "byId succeeds" { Expect.equal diff --git a/src/Tests/PostgresExtensionTests.fs b/src/Tests/PostgresExtensionTests.fs index 220463a..4d215b2 100644 --- a/src/Tests/PostgresExtensionTests.fs +++ b/src/Tests/PostgresExtensionTests.fs @@ -486,14 +486,14 @@ let integrationTests = Expect.isNone doc "There should not have been a document returned" } ] - testList "updateFull" [ + testList "updateById" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! conn.updateFull PostgresDb.TableName "one" testDoc + do! conn.updateById PostgresDb.TableName "one" testDoc let! after = conn.findById 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" @@ -505,17 +505,17 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updateFull + do! conn.updateById PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] - testList "updateFullFunc" [ + testList "updateByFunc" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.updateFullFunc + do! conn.updateByFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } let! after = conn.findById PostgresDb.TableName "one" Expect.isSome after "There should have been a document returned post-update" @@ -531,17 +531,17 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updateFullFunc + do! conn.updateByFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] - testList "updatePartialById" [ + testList "patchById" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.updatePartialById PostgresDb.TableName "one" {| NumValue = 44 |} + do! conn.patchById PostgresDb.TableName "one" {| NumValue = 44 |} let! after = conn.findById 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" @@ -553,16 +553,16 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialById PostgresDb.TableName "test" {| Foo = "green" |} + do! conn.patchById PostgresDb.TableName "test" {| Foo = "green" |} } ] - testList "updatePartialByField" [ + testList "patchByField" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.updatePartialByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + do! conn.patchByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} let! after = conn.countByField PostgresDb.TableName "NumValue" EQ "77" Expect.equal after 2 "There should have been 2 documents returned" } @@ -573,16 +573,16 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + do! conn.patchByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] - testList "updatePartialByContains" [ + testList "patchByContains" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.updatePartialByContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} + do! conn.patchByContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} let! after = conn.countByContains PostgresDb.TableName {| NumValue = 77 |} Expect.equal after 2 "There should have been 2 documents returned" } @@ -593,16 +593,16 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + do! conn.patchByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} } ] - testList "updatePartialByJsonPath" [ + testList "patchByJsonPath" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() use conn = mkConn db do! loadDocs conn - do! conn.updatePartialByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} + do! conn.patchByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} let! after = conn.countByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 999)" Expect.equal after 2 "There should have been 2 documents returned" } @@ -613,7 +613,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} + do! conn.patchByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} } ] testList "deleteById" [ diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 494c896..66938f6 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -101,28 +101,28 @@ let unitTests = "SELECT by JSON Path match query not correct" } ] - testList "Update" [ - test "partialById succeeds" { + testList "Patch" [ + test "byId succeeds" { Expect.equal - (Query.Update.partialById PostgresDb.TableName) + (Query.Patch.byId PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" "UPDATE partial by ID statement not correct" } - test "partialByField succeeds" { + test "byField succeeds" { Expect.equal - (Query.Update.partialByField PostgresDb.TableName "Snail" LT) + (Query.Patch.byField PostgresDb.TableName "Snail" LT) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field" "UPDATE partial by ID statement not correct" } - test "partialByContains succeeds" { + test "byContains succeeds" { Expect.equal - (Query.Update.partialByContains PostgresDb.TableName) + (Query.Patch.byContains PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" "UPDATE partial by JSON containment statement not correct" } - test "partialByJsonPath succeeds" { + test "byJsonPath succeeds" { Expect.equal - (Query.Update.partialByJsonPath PostgresDb.TableName) + (Query.Patch.byJsonPath PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" "UPDATE partial by JSON Path statement not correct" } @@ -606,13 +606,13 @@ let integrationTests = ] ] testList "Update" [ - testList "full" [ + 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.full PostgresDb.TableName "one" testDoc + 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" @@ -624,18 +624,18 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.full + do! Update.byId PostgresDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] - testList "fullFunc" [ + testList "byFunc" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Update.fullFunc PostgresDb.TableName (_.Id) + 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" @@ -651,16 +651,18 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.fullFunc + do! Update.byFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] - testList "partialById" [ + ] + testList "Patch" [ + testList "byId" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Update.partialById PostgresDb.TableName "one" {| NumValue = 44 |} + 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" @@ -672,15 +674,15 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialById PostgresDb.TableName "test" {| Foo = "green" |} + do! Patch.byId PostgresDb.TableName "test" {| Foo = "green" |} } ] - testList "partialByField" [ + testList "byField" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Update.partialByField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + do! Patch.byField PostgresDb.TableName "Value" EQ "purple" {| NumValue = 77 |} let! after = Count.byField PostgresDb.TableName "NumValue" EQ "77" Expect.equal after 2 "There should have been 2 documents returned" } @@ -691,15 +693,15 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + do! Patch.byField PostgresDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] - testList "partialByContains" [ + testList "byContains" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Update.partialByContains PostgresDb.TableName {| Value = "purple" |} {| NumValue = 77 |} + 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" } @@ -710,15 +712,15 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} + do! Patch.byContains PostgresDb.TableName {| Value = "burgundy" |} {| Foo = "green" |} } ] - testList "partialByJsonPath" [ + testList "byJsonPath" [ testTask "succeeds when a document is updated" { use db = PostgresDb.BuildDb() do! loadDocs () - do! Update.partialByJsonPath PostgresDb.TableName "$.NumValue ? (@ > 10)" {| NumValue = 1000 |} + 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" } @@ -729,7 +731,7 @@ let integrationTests = Expect.equal before 0 "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} + do! Patch.byJsonPath PostgresDb.TableName "$.NumValue ? (@ < 0)" {| Foo = "green" |} } ] ] -- 2.45.1 From c6b3d275b0a94fb872d09d01db29f508131d02e1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 28 Dec 2023 20:28:02 -0500 Subject: [PATCH 23/26] Change "update partial" to "patch" for SQLite --- src/Sqlite/Extensions.fs | 48 ++++----- src/Sqlite/Library.fs | 100 ++++++++++-------- .../SqliteCSharpExtensionTests.cs | 24 ++--- src/Tests.CSharp/SqliteCSharpTests.cs | 39 +++---- src/Tests/SqliteExtensionTests.fs | 32 +++--- src/Tests/SqliteTests.fs | 38 ++++--- 6 files changed, 144 insertions(+), 137 deletions(-) diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 57ed75f..509669c 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -72,21 +72,21 @@ module Extensions = member conn.findFirstByField<'TDoc> tableName fieldName op (value: obj) = WithConn.Find.firstByField<'TDoc> tableName fieldName op value conn - /// Update an entire document - member conn.updateFull tableName (docId: 'TKey) (document: 'TDoc) = - WithConn.Update.full tableName docId document conn + /// Update an entire document by its ID + member conn.updateById tableName (docId: 'TKey) (document: 'TDoc) = + WithConn.Update.byId tableName docId document conn - /// Update an entire document - member conn.updateFullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = - WithConn.Update.fullFunc tableName idFunc document conn + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + member conn.updateByFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithConn.Update.byFunc tableName idFunc document conn - /// Update a partial document - member conn.updatePartialById tableName (docId: 'TKey) (partial: 'TPatch) = - WithConn.Update.partialById tableName docId partial conn + /// Patch a document by its ID + member conn.patchById tableName (docId: 'TKey) (patch: 'TPatch) = + WithConn.Patch.byId tableName docId patch conn - /// Update partial documents using a comparison on a JSON field - member conn.updatePartialByField tableName fieldName op (value: obj) (partial: 'TPatch) = - WithConn.Update.partialByField tableName fieldName op value partial conn + /// Patch documents using a comparison on a JSON field + member conn.patchByField tableName fieldName op (value: obj) (patch: 'TPatch) = + WithConn.Patch.byField tableName fieldName op value patch conn /// Delete a document by its ID member conn.deleteById tableName (docId: 'TKey) = @@ -184,25 +184,25 @@ type SqliteConnectionCSharpExtensions = static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, fieldName, op, value: obj) = WithConn.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, conn) - /// Update an entire document + /// Update an entire document by its ID [] - static member inline UpdateFull<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = - WithConn.Update.full tableName docId document conn + static member inline UpdateById<'TKey, 'TDoc>(conn, tableName, docId: 'TKey, document: 'TDoc) = + WithConn.Update.byId tableName docId document conn - /// Update an entire document + /// Update an entire document by its ID, using the provided function to obtain the ID from the document [] - static member inline UpdateFullFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) = - WithConn.Update.FullFunc(tableName, idFunc, doc, conn) + static member inline UpdateByFunc<'TKey, 'TDoc>(conn, tableName, idFunc: System.Func<'TDoc, 'TKey>, doc: 'TDoc) = + WithConn.Update.ByFunc(tableName, idFunc, doc, conn) - /// Update a partial document + /// Patch a document by its ID [] - static member inline UpdatePartialById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, partial: 'TPatch) = - WithConn.Update.partialById tableName docId partial conn + static member inline PatchById<'TKey, 'TPatch>(conn, tableName, docId: 'TKey, patch: 'TPatch) = + WithConn.Patch.byId tableName docId patch conn - /// Update partial documents using a comparison on a JSON field + /// Patch documents using a comparison on a JSON field [] - static member inline UpdatePartialByField<'TPatch>(conn, tableName, fieldName, op, value: obj, partial: 'TPatch) = - WithConn.Update.partialByField tableName fieldName op value partial conn + static member inline PatchByField<'TPatch>(conn, tableName, fieldName, op, value: obj, patch: 'TPatch) = + WithConn.Patch.byField tableName fieldName op value patch conn /// Delete a document by its ID [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 9920d15..c80ad11 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -39,17 +39,17 @@ module Query = let ensureTable name = Query.Definition.ensureTableFor name "TEXT" - /// Document update queries - module Update = + /// Document patching (partial update) queries + module Patch = - /// Query to update a partial document by its ID - [] - let partialById tableName = + /// Query to patch (partially update) a document by its ID + [] + let byId tableName = $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {Query.whereById "@id"}""" - /// Query to update a partial document via a comparison on a JSON field - [] - let partialByField tableName fieldName op = + /// Query to patch (partially update) a document via a comparison on a JSON field + [] + let byField tableName fieldName op = sprintf "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" tableName (Query.whereByField fieldName op "@field") @@ -295,32 +295,34 @@ module WithConn = [] module Update = - /// Update an entire document - [] - let full tableName (docId: 'TKey) (document: 'TDoc) conn = + /// 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 - /// Update an entire document - [] - let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn = - full tableName (idFunc document) document conn + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + [] + let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) conn = + byId tableName (idFunc document) document conn - /// Update an entire document - let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) = - fullFunc tableName idFunc.Invoke document conn + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, conn) = + byFunc tableName idFunc.Invoke document conn + + /// Commands to patch (partially update) documents + [] + module Patch = - /// Update a partial document - [] - let partialById tableName (docId: 'TKey) (partial: 'TPatch) conn = - Custom.nonQuery (Query.Update.partialById tableName) [ idParam docId; jsonParam "@data" partial ] conn + /// 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 - /// Update partial documents using a comparison on a JSON field - [] - let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) (conn: SqliteConnection) = + /// Patch documents using a comparison on a JSON field + [] + let byField tableName fieldName op (value: obj) (patch: 'TPatch) (conn: SqliteConnection) = Custom.nonQuery - (Query.Update.partialByField tableName fieldName op) - [ fieldParam value; jsonParam "@data" partial ] - conn + (Query.Patch.byField tableName fieldName op) [ fieldParam value; jsonParam "@data" patch ] conn /// Commands to delete documents [] @@ -490,34 +492,38 @@ module Find = [] module Update = - /// Update an entire document - [] - let full tableName (docId: 'TKey) (document: 'TDoc) = + /// Update an entire document by its ID + [] + let byId tableName (docId: 'TKey) (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.Update.full tableName docId document conn + WithConn.Update.byId tableName docId document conn - /// Update an entire document - [] - let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + [] + let byFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.Update.fullFunc tableName idFunc document conn + WithConn.Update.byFunc tableName idFunc document conn - /// Update an entire document - let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + /// Update an entire document by its ID, using the provided function to obtain the ID from the document + let ByFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.Update.FullFunc(tableName, idFunc, document, conn) + WithConn.Update.ByFunc(tableName, idFunc, document, conn) + +/// Commands to patch (partially update) documents +[] +module Patch = - /// Update a partial document - [] - let partialById tableName (docId: 'TKey) (partial: 'TPatch) = + /// Patch a document by its ID + [] + let byId tableName (docId: 'TKey) (patch: 'TPatch) = use conn = Configuration.dbConn () - WithConn.Update.partialById tableName docId partial conn + WithConn.Patch.byId tableName docId patch conn - /// Update partial documents using a comparison on a JSON field in the WHERE clause - [] - let partialByField tableName fieldName op (value: obj) (partial: 'TPatch) = + /// Patch documents using a comparison on a JSON field in the WHERE clause + [] + let byField tableName fieldName op (value: obj) (patch: 'TPatch) = use conn = Configuration.dbConn () - WithConn.Update.partialByField tableName fieldName op value partial conn + WithConn.Patch.byField tableName fieldName op value patch conn /// Commands to delete documents [] diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index 54123ef..e48def8 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -348,7 +348,7 @@ public static class SqliteCSharpExtensionTests Expect.isNull(doc, "There should not have been a document returned"); }) }), - TestList("UpdateFull", new[] + TestList("UpdateById", new[] { TestCase("succeeds when a document is updated", async () => { @@ -357,7 +357,7 @@ public static class SqliteCSharpExtensionTests await LoadDocs(); var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await conn.UpdateFull(SqliteDb.TableName, "one", testDoc); + await conn.UpdateById(SqliteDb.TableName, "one", testDoc); var after = await conn.FindById(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"); @@ -373,11 +373,11 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdateFull(SqliteDb.TableName, "test", + await conn.UpdateById(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - TestList("UpdateFullFunc", new[] + TestList("UpdateByFunc", new[] { TestCase("succeeds when a document is updated", async () => { @@ -385,7 +385,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); var after = await conn.FindById(SqliteDb.TableName, "one"); Expect.isNotNull(after, "There should have been a document returned post-update"); @@ -402,11 +402,11 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdateFullFunc(SqliteDb.TableName, doc => doc.Id, + await conn.UpdateByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) }), - TestList("UpdatePartialById", new[] + TestList("PatchById", new[] { TestCase("succeeds when a document is updated", async () => { @@ -414,7 +414,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.UpdatePartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + await conn.PatchById(SqliteDb.TableName, "one", new { NumValue = 44 }); var after = await conn.FindById(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"); @@ -428,10 +428,10 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + await conn.PatchById(SqliteDb.TableName, "test", new { Foo = "green" }); }) }), - TestList("UpdatePartialByField", new[] + TestList("PatchByField", new[] { TestCase("succeeds when a document is updated", async () => { @@ -439,7 +439,7 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); await LoadDocs(); - await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); + await conn.PatchByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); var after = await conn.CountByField(SqliteDb.TableName, "NumValue", Op.EQ, 77); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), @@ -451,7 +451,7 @@ public static class SqliteCSharpExtensionTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await conn.UpdatePartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); + await conn.PatchByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); }) }), TestList("DeleteById", new[] diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index a511856..9e68118 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -25,17 +25,17 @@ public static class SqliteCSharpTests Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); }), - TestList("Update", new[] + TestList("Patch", new[] { - TestCase("PartialById succeeds", () => + TestCase("ById succeeds", () => { - Expect.equal(Sqlite.Query.Update.PartialById("tbl"), + 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("PartialByField succeeds", () => + TestCase("ByField succeeds", () => { - Expect.equal(Sqlite.Query.Update.PartialByField("tbl", "Part", Op.NE), + Expect.equal(Sqlite.Query.Patch.ByField("tbl", "Part", Op.NE), "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", "UPDATE partial by JSON comparison query not correct"); }) @@ -420,7 +420,7 @@ public static class SqliteCSharpTests }), TestList("Update", new[] { - TestList("Full", new[] + TestList("ById", new[] { TestCase("succeeds when a document is updated", async () => { @@ -428,7 +428,7 @@ public static class SqliteCSharpTests await LoadDocs(); var testDoc = new JsonDocument { Id = "one", Sub = new() { Foo = "blue", Bar = "red" } }; - await Update.Full(SqliteDb.TableName, "one", testDoc); + 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"); @@ -444,18 +444,18 @@ public static class SqliteCSharpTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await Update.Full(SqliteDb.TableName, "test", + await Update.ById(SqliteDb.TableName, "test", new JsonDocument { Id = "x", Sub = new() { Foo = "blue", Bar = "red" } }); }) }), - TestList("FullFunc", new[] + TestList("ByFunc", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, + 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"); @@ -472,18 +472,21 @@ public static class SqliteCSharpTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await Update.FullFunc(SqliteDb.TableName, doc => doc.Id, + await Update.ByFunc(SqliteDb.TableName, doc => doc.Id, new JsonDocument { Id = "one", Value = "le un", NumValue = 1 }); }) }), - TestList("PartialById", new[] + }), + TestList("Patch", new[] + { + TestList("ById", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Update.PartialById(SqliteDb.TableName, "one", new { NumValue = 44 }); + 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"); @@ -497,18 +500,17 @@ public static class SqliteCSharpTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialById(SqliteDb.TableName, "test", new { Foo = "green" }); + await Patch.ById(SqliteDb.TableName, "test", new { Foo = "green" }); }) }), - TestList("PartialByField", new[] + TestList("ByField", new[] { TestCase("succeeds when a document is updated", async () => { await using var db = await SqliteDb.BuildDb(); await LoadDocs(); - await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "purple", - new { NumValue = 77 }); + await Patch.ByField(SqliteDb.TableName, "Value", Op.EQ, "purple", new { NumValue = 77 }); var after = await Count.ByField(SqliteDb.TableName, "NumValue", Op.EQ, 77); Expect.equal(after, 2L, "There should have been 2 documents returned"); }), @@ -520,8 +522,7 @@ public static class SqliteCSharpTests Expect.isEmpty(before, "There should have been no documents returned"); // This not raising an exception is the test - await Update.PartialByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", - new { Foo = "green" }); + await Patch.ByField(SqliteDb.TableName, "Value", Op.EQ, "burgundy", new { Foo = "green" }); }) }) }), diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index 2d43813..d53c72c 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -231,14 +231,14 @@ let integrationTests = Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] - testList "updateFull" [ + testList "updateById" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } - do! conn.updateFull SqliteDb.TableName "one" testDoc + do! conn.updateById SqliteDb.TableName "one" testDoc let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" @@ -252,22 +252,20 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.updateFull + do! conn.updateById SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] - testList "updateFullFunc" [ + testList "updateByFunc" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.updateFullFunc - SqliteDb.TableName - (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + do! conn.updateByFunc + SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" @@ -284,19 +282,17 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.updateFullFunc - SqliteDb.TableName - (_.Id) - { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + do! conn.updateByFunc + SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] - testList "updatePartialById" [ + testList "patchById" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.updatePartialById SqliteDb.TableName "one" {| NumValue = 44 |} + do! conn.patchById SqliteDb.TableName "one" {| NumValue = 44 |} let! after = conn.findById SqliteDb.TableName "one" if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" @@ -310,16 +306,16 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialById SqliteDb.TableName "test" {| Foo = "green" |} + do! conn.patchById SqliteDb.TableName "test" {| Foo = "green" |} } ] - testList "updatePartialByField" [ + testList "patchByField" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () do! loadDocs () - do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + do! conn.patchByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} let! after = conn.countByField SqliteDb.TableName "NumValue" EQ 77 Expect.equal after 2L "There should have been 2 documents returned" } @@ -331,7 +327,7 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! conn.updatePartialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + do! conn.patchByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] testList "deleteById" [ diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 819be57..14dfee3 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -17,16 +17,16 @@ let unitTests = "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" "CREATE TABLE statement not correct" } - testList "Update" [ - test "partialById succeeds" { + testList "Patch" [ + test "byId succeeds" { Expect.equal - (Query.Update.partialById "tbl") + (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 "partialByField succeeds" { + test "byField succeeds" { Expect.equal - (Query.Update.partialByField "tbl" "Part" NE) + (Query.Patch.byField "tbl" "Part" NE) "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" "UPDATE partial by JSON comparison query not correct" } @@ -385,13 +385,13 @@ let integrationTests = ] ] testList "Update" [ - testList "full" [ + 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.full SqliteDb.TableName "one" testDoc + 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" @@ -403,18 +403,19 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! Update.full + do! Update.byId SqliteDb.TableName "test" { emptyDoc with Id = "x"; Sub = Some { Foo = "blue"; Bar = "red" } } } ] - testList "fullFunc" [ + testList "byFunc" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + 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 @@ -429,15 +430,18 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! Update.fullFunc SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } + do! Update.byFunc + SqliteDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } } ] - testList "partialById" [ + ] + testList "Patch" [ + testList "byId" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.partialById SqliteDb.TableName "one" {| NumValue = 44 |} + 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" @@ -449,15 +453,15 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialById SqliteDb.TableName "test" {| Foo = "green" |} + do! Patch.byId SqliteDb.TableName "test" {| Foo = "green" |} } ] - testList "partialByField" [ + testList "byField" [ testTask "succeeds when a document is updated" { use! db = SqliteDb.BuildDb() do! loadDocs () - do! Update.partialByField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} + do! Patch.byField SqliteDb.TableName "Value" EQ "purple" {| NumValue = 77 |} let! after = Count.byField SqliteDb.TableName "NumValue" EQ 77 Expect.equal after 2L "There should have been 2 documents returned" } @@ -468,7 +472,7 @@ let integrationTests = Expect.isEmpty before "There should have been no documents returned" // This not raising an exception is the test - do! Update.partialByField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} + do! Patch.byField SqliteDb.TableName "Value" EQ "burgundy" {| Foo = "green" |} } ] ] -- 2.45.1 From e4add066488b5981f9b4ba354da6663e8c76ca78 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 28 Dec 2023 20:30:03 -0500 Subject: [PATCH 24/26] Remove "Test" project --- src/BitBadger.Documents.sln | 6 ------ src/Test/Class1.cs | 12 ------------ src/Test/Test.csproj | 14 -------------- 3 files changed, 32 deletions(-) delete mode 100644 src/Test/Class1.cs delete mode 100644 src/Test/Test.csproj diff --git a/src/BitBadger.Documents.sln b/src/BitBadger.Documents.sln index 4012c67..c6f850d 100644 --- a/src/BitBadger.Documents.sln +++ b/src/BitBadger.Documents.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Common", "Common\BitBadger.Documents.Common.fsproj", "{E52D624A-2A1F-4D38-82B6-115907D9CB1A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}" -EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Tests", "Tests\BitBadger.Documents.Tests.fsproj", "{45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}" EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BitBadger.Documents.Sqlite", "Sqlite\BitBadger.Documents.Sqlite.fsproj", "{B8A82483-1E72-46D2-B29A-1C371AC5DD20}" @@ -28,10 +26,6 @@ Global {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Debug|Any CPU.Build.0 = Debug|Any CPU {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {E52D624A-2A1F-4D38-82B6-115907D9CB1A}.Release|Any CPU.Build.0 = Release|Any CPU - {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0419E91C-2CC5-4FCC-BAB6-1074EFD9C073}.Release|Any CPU.Build.0 = Release|Any CPU {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Debug|Any CPU.Build.0 = Debug|Any CPU {45D5D503-9FD0-4ADB-ABC6-C8FDAD82B848}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Test/Class1.cs b/src/Test/Class1.cs deleted file mode 100644 index b96c733..0000000 --- a/src/Test/Class1.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Test; - -using BitBadger.Documents; - -public class Class1 -{ - public void Toot() - { - var ticket = Query.WhereByField("test", Op.GE, ""); - var others = Query.Count.All("howdy"); - } -} diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj deleted file mode 100644 index 7153813..0000000 --- a/src/Test/Test.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - -- 2.45.1 From 238c68de570ba3c4a84afff8b0cf07959444fc9f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 31 Dec 2023 06:58:10 -0500 Subject: [PATCH 25/26] Add CI for project Migrated from BitBadger.Npgsql.Documents --- .github/workflows/ci.yml | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f194f7c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-and-test: + + runs-on: ubuntu-latest + + strategy: + matrix: + dotnet-version: [ "6.0", "7.0", "8.0" ] + postgres-version: [ "12", "13", "14", "15", "latest" ] + + services: + postgres: + image: postgres:${{ matrix.postgres-version }} + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET ${{ matrix.dotnet-version }}.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ matrix.dotnet-version }}.x + - name: Restore dependencies + run: dotnet restore src/BitBadger.Documents.sln + - name: Build + run: dotnet build src/BitBadger.Documents.sln --no-restore + - name: Test ${{ matrix.dotnet-version }} against PostgreSQL ${{ matrix.postgres-version }} + run: dotnet run --project src/Tests/BitBadger.Documents.Tests.fsproj -f net${{ matrix.dotnet-version }} + publish: + runs-on: ubuntu-latest + needs: build-and-test + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "8.0" + - name: Package Common Library + run: dotnet pack src/Common/BitBadger.Documents.Common.fsproj -c Release + - name: Move Common package + run: cp src/Common/bin/Release/BitBadger.Documents.Common.*.nupkg . + - name: Package PostgreSQL Library + run: dotnet pack src/Postgres/BitBadger.Documents.Postgres.fsproj -c Release + - name: Move PostgreSQL package + run: cp src/Postgres/bin/Release/BitBadger.Documents.Postgres.*.nupkg . + - name: Package SQLite Library + run: dotnet pack src/Sqlite/BitBadger.Documents.Sqlite.fsproj -c Release + - name: Move SQLite package + run: cp src/Sqlite/bin/Release/BitBadger.Documents.Sqlite.*.nupkg . + - name: Save Packages + uses: actions/upload-artifact@v3 + with: + name: packages + path: | + *.nupkg -- 2.45.1 From 52c00d2485767b195961bfa06cec5e75958ada06 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 31 Dec 2023 07:29:26 -0500 Subject: [PATCH 26/26] Add missing index function for SQLite - Add toCount and toExists for SQLite results --- src/Sqlite/Extensions.fs | 8 ++-- src/Sqlite/Library.fs | 47 +++++++++---------- .../SqliteCSharpExtensionTests.cs | 23 +++++++-- src/Tests.CSharp/SqliteCSharpTests.cs | 20 ++++++-- src/Tests/SqliteExtensionTests.fs | 30 ++++++++---- src/Tests/SqliteTests.fs | 29 ++++++++---- 6 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 509669c..91304b5 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -29,8 +29,8 @@ module Extensions = WithConn.Definition.ensureTable name conn /// Create an index on a document table - member conn.ensureIndex tableName indexName fields = - WithConn.Definition.ensureIndex tableName indexName fields conn + member conn.ensureFieldIndex tableName indexName fields = + WithConn.Definition.ensureFieldIndex tableName indexName fields conn /// Insert a new document member conn.insert<'TDoc> tableName (document: 'TDoc) = @@ -131,8 +131,8 @@ type SqliteConnectionCSharpExtensions = /// Create an index on one or more fields in a document table [] - static member inline EnsureIndex(conn, tableName, indexName, fields) = - WithConn.Definition.ensureIndex tableName indexName fields conn + static member inline EnsureFieldIndex(conn, tableName, indexName, fields) = + WithConn.Definition.ensureFieldIndex tableName indexName fields conn /// Insert a new document [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index c80ad11..6a9c4a2 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -103,21 +103,16 @@ module Results = it <- Seq.append it (Seq.singleton (mapFunc rdr)) return List.ofSeq it } - - /// Create a list of items for the results of the given command, using the specified mapping function - let ToCustomList<'TDoc>(cmd, mapFunc: System.Func) = backgroundTask { - let! results = toCustomList<'TDoc> cmd mapFunc.Invoke - return ResizeArray<'TDoc> results - } - - /// Create a list of items for the results of the given command - [] - let toDocumentList<'TDoc> (cmd: SqliteCommand) = - toCustomList<'TDoc> cmd fromData - - /// Create a list of items for the results of the given command - let ToDocumentList<'TDoc> cmd = - ToCustomList<'TDoc>(cmd, fromData<'TDoc>) + + /// Extract a count from the first column + [] + let toCount (row: SqliteDataReader) = + row.GetInt64 0 + + /// Extract a true/false value from a count in the first column + [] + let toExists row = + toCount(row) > 0L [] @@ -203,8 +198,8 @@ module WithConn = } /// Create an index on a document table - [] - let ensureIndex tableName indexName fields conn = + [] + let ensureFieldIndex tableName indexName fields conn = Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] conn /// Insert a new document @@ -224,30 +219,26 @@ module WithConn = /// Count all documents in a table [] let all tableName conn = - Custom.scalar (Query.Count.all tableName) [] (_.GetInt64(0)) conn + Custom.scalar (Query.Count.all tableName) [] toCount conn /// Count matching documents using a comparison on a JSON field [] let byField tableName fieldName op (value: obj) conn = - Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] (_.GetInt64(0)) conn + Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] toCount conn /// Commands to determine if documents exist [] module Exists = - /// SQLite returns a 0 for not-exists and 1 for exists - let private exists (rdr: SqliteDataReader) = - rdr.GetInt64(0) > 0 - /// Determine if a document exists for the given ID [] let byId tableName (docId: 'TKey) conn = - Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] exists 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 fieldName op (value: obj) conn = - Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] exists conn + Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] toExists conn /// Commands to retrieve documents [] @@ -391,6 +382,12 @@ module Definition = let ensureTable name = use conn = Configuration.dbConn () WithConn.Definition.ensureTable name conn + + /// Create an index on a document table + [] + let ensureFieldIndex tableName indexName fields = + use conn = Configuration.dbConn () + WithConn.Definition.ensureFieldIndex tableName indexName fields conn /// Document insert/save functions [] diff --git a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs index e48def8..cc5fb5f 100644 --- a/src/Tests.CSharp/SqliteCSharpExtensionTests.cs +++ b/src/Tests.CSharp/SqliteCSharpExtensionTests.cs @@ -108,12 +108,9 @@ public static class SqliteCSharpExtensionTests await using var conn = Sqlite.Configuration.DbConn(); Func> itExists = async name => - { - var result = await conn.CustomScalar( + await conn.CustomScalar( $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); - return result > 0L; - }; + new SqliteParameter[] { new("@name", name) }, Results.ToExists); var exists = await itExists("ensured"); var alsoExists = await itExists("idx_ensured_key"); @@ -127,6 +124,22 @@ public static class SqliteCSharpExtensionTests Expect.isTrue(exists, "The table should now exist"); Expect.isTrue(alsoExists, "The key index should now exist"); }), + 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(); + Expect.isFalse(exists, "The index should not exist already"); + + await conn.EnsureTable("ensured"); + await conn.EnsureFieldIndex("ensured", "test", new[] { "Id", "Category" }); + exists = await indexExists(); + Expect.isTrue(exists, "The index should now exist"); + }), TestList("Insert", new[] { TestCase("succeeds", async () => diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index 9e68118..f2e0621 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -200,11 +200,25 @@ public static class SqliteCSharpTests async ValueTask ItExists(string name) { - var result = await Custom.Scalar( + return await Custom.Scalar( $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it", - new SqliteParameter[] { new("@name", name) }, rdr => rdr.GetInt64(0)); - return result > 0L; + 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[] diff --git a/src/Tests/SqliteExtensionTests.fs b/src/Tests/SqliteExtensionTests.fs index d53c72c..4c6e2b5 100644 --- a/src/Tests/SqliteExtensionTests.fs +++ b/src/Tests/SqliteExtensionTests.fs @@ -16,14 +16,11 @@ let integrationTests = testTask "ensureTable succeeds" { use! db = SqliteDb.BuildDb() use conn = Configuration.dbConn () - let itExists (name: string) = task { - let! result = - conn.customScalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" - [ SqliteParameter("@name", name) ] - _.GetInt64(0) - return result > 0 - } + let itExists (name: string) = + conn.customScalar + $"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" @@ -36,6 +33,23 @@ let integrationTests = Expect.isTrue exists' "The table should now exist" Expect.isTrue alsoExists' "The key index should now exist" } + testTask "ensureFieldIndex succeeds" { + use! db = SqliteDb.BuildDb() + use conn = Configuration.dbConn () + let indexExists () = + conn.customScalar + $"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! conn.ensureTable "ensured" + do! conn.ensureFieldIndex "ensured" "test" [ "Name"; "Age" ] + let! exists' = indexExists () + Expect.isTrue exists' "The index should now exist" + } testList "insert" [ testTask "succeeds" { use! db = SqliteDb.BuildDb() diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 14dfee3..caaa437 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -184,14 +184,11 @@ let integrationTests = testList "Definition" [ testTask "ensureTable succeeds" { use! db = SqliteDb.BuildDb() - let itExists (name: string) = task { - let! result = - Custom.scalar - $"SELECT EXISTS (SELECT 1 FROM {SqliteDb.Catalog} WHERE name = @name) AS it" - [ SqliteParameter("@name", name) ] - _.GetInt64(0) - return result > 0 - } + 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" @@ -204,6 +201,22 @@ let integrationTests = 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" { -- 2.45.1