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 ]