diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 6a9c4a2..6be811e 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -53,6 +53,21 @@ module Query = sprintf "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" tableName (Query.whereByField fieldName op "@field") + + /// Queries to remove a field from a document + module RemoveField = + + /// Query to remove a field from a document by the document's ID + [] + let byId tableName = + $"""UPDATE %s{tableName} SET data = json_remove(data, @name) WHERE {Query.whereById "@id"}""" + + /// Query to remove a field from a document via a comparison on a JSON field within the document + [] + let byField tableName fieldName op = + sprintf + "UPDATE %s SET data = json_remove(data, @name) WHERE %s" + tableName (Query.whereByField fieldName op "@field") /// Parameter handling helpers @@ -74,6 +89,11 @@ module Parameters = let fieldParam (value: obj) = SqliteParameter("@field", value) + /// Create a JSON field name parameter (name "@name") + [] + let fieldNameParam name = + SqliteParameter("@name", $"$.%s{name}") + /// An empty parameter sequence [] let noParams = @@ -315,6 +335,23 @@ module WithConn = Custom.nonQuery (Query.Patch.byField tableName fieldName op) [ fieldParam value; jsonParam "@data" patch ] conn + /// Commands to remove fields from documents + [] + module RemoveField = + + /// Remove a field from a document by the document's ID + [] + let byId tableName (docId: 'TKey) fieldName conn = + Custom.nonQuery (Query.RemoveField.byId tableName) [ idParam docId; fieldNameParam fieldName ] conn + + /// Remove a field from a document via a comparison on a JSON field in the document + [] + let byField tableName whereFieldName op (value: obj) removeFieldName conn = + Custom.nonQuery + (Query.RemoveField.byField tableName whereFieldName op) + [ fieldParam value; fieldNameParam removeFieldName ] + conn + /// Commands to delete documents [] module Delete = @@ -522,6 +559,22 @@ module Patch = use conn = Configuration.dbConn () WithConn.Patch.byField tableName fieldName op value patch conn +/// Commands to remove fields from documents +[] +module RemoveField = + + /// Remove a field from a document by the document's ID + [] + let byId tableName (docId: 'TKey) fieldName = + use conn = Configuration.dbConn () + WithConn.RemoveField.byId tableName docId fieldName conn + + /// Remove a field from a document via a comparison on a JSON field in the document + [] + let byField tableName whereFieldName op (value: obj) removeFieldName = + use conn = Configuration.dbConn () + WithConn.RemoveField.byField tableName whereFieldName op value removeFieldName conn + /// Commands to delete documents [] module Delete = diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index f2e0621..91fb9ce 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -1,4 +1,5 @@ -using Expecto.CSharp; +using System.Text.Json; +using Expecto.CSharp; using Expecto; using Microsoft.Data.Sqlite; using Microsoft.FSharp.Core; @@ -40,6 +41,21 @@ public static class SqliteCSharpTests "UPDATE partial by JSON comparison query not correct"); }) }), + TestList("RemoveField", new[] + { + TestCase("ById succeeds", () => + { + Expect.equal(Sqlite.Query.RemoveField.ById("tbl"), + "UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id", + "Remove field by ID query not correct"); + }), + TestCase("ByField succeeds", () => + { + Expect.equal(Sqlite.Query.RemoveField.ByField("tbl", "Fly", Op.LT), + "UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Fly' < @field", + "Remove field by field query not correct"); + }) + }) }), TestList("Parameters", new[] { @@ -540,6 +556,37 @@ public static class SqliteCSharpTests }) }) }), + TestList("RemoveField", new[] + { + TestList("ById", new[] + { + TestCase("succeeds when a field is removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + await RemoveField.ById(SqliteDb.TableName, "two", "Sub"); + var updated = await Find.ById(SqliteDb.TableName, "two"); + Expect.isNotNull(updated, "The updated document should have been retrieved"); + Expect.isNull(updated.Sub, "The sub-document should have been removed"); + }), + TestCase("succeeds when a field is not removed", async () => + { + await using var db = await SqliteDb.BuildDb(); + await LoadDocs(); + + // This not raising an exception is the test + await RemoveField.ById(SqliteDb.TableName, "two", "AFieldThatIsNotThere"); + }), + TestCase("succeeds when no document is matched", async () => + { + await using var db = await SqliteDb.BuildDb(); + + // This not raising an exception is the test + await RemoveField.ById(SqliteDb.TableName, "two", "Value"); + }) + }) + }), TestList("Delete", new[] { TestList("ById", new[] diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index caaa437..582b2b1 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -1,5 +1,6 @@ module SqliteTests +open System.Text.Json open BitBadger.Documents open BitBadger.Documents.Sqlite open BitBadger.Documents.Tests @@ -31,6 +32,20 @@ let unitTests = "UPDATE partial by JSON comparison query not correct" } ] + testList "RemoveField" [ + test "byId succeeds" { + Expect.equal + (Query.RemoveField.byId "tbl") + "UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Id' = @id" + "Remove field by ID query not correct" + } + test "byField succeeds" { + Expect.equal + (Query.RemoveField.byField "tbl" "Fly" GT) + "UPDATE tbl SET data = json_remove(data, @name) WHERE data ->> 'Fly' > @field" + "Remove field by field query not correct" + } + ] ] testList "Parameters" [ test "idParam succeeds" { @@ -489,6 +504,35 @@ let integrationTests = } ] ] + testList "RemoveField" [ + testList "byId" [ + testTask "succeeds when a field is removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + do! RemoveField.byId SqliteDb.TableName "two" "Sub" + try + let! _ = Find.byId SqliteDb.TableName "two" + Expect.isTrue false "The updated document should have failed to parse" + with + | :? JsonException -> () + | exn as ex -> Expect.isTrue false $"Threw {ex.GetType().Name} ({ex.Message})" + } + testTask "succeeds when a field is not removed" { + use! db = SqliteDb.BuildDb() + do! loadDocs () + + // This not raising an exception is the test + do! RemoveField.byId SqliteDb.TableName "two" "AFieldThatIsNotThere" + } + testTask "succeeds when no document is matched" { + use! db = SqliteDb.BuildDb() + + // This not raising an exception is the test + do! RemoveField.byId SqliteDb.TableName "two" "Value" + } + ] + ] testList "Delete" [ testList "byId" [ testTask "succeeds when a document is deleted" {