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 ]