From bb4252f3c188e1273b6143a6dba3699a01b9dd81 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 19 Dec 2023 18:30:34 -0500 Subject: [PATCH] WIP on SQLite doc library --- src/MyWebLog.Data/SQLite/Helpers.fs | 135 +++++------ .../SQLite/SQLiteCategoryData.fs | 125 ++++------ src/MyWebLog.Data/SQLite/SQLitePageData.fs | 167 ++++++------- src/MyWebLog.Data/SQLite/SQLitePostData.fs | 226 ++++++++---------- src/MyWebLog.Data/SQLite/SQLiteTagMapData.fs | 6 +- src/MyWebLog.Data/SQLite/SQLiteThemeData.fs | 4 +- src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs | 4 +- .../SQLite/SQLiteWebLogUserData.fs | 10 +- src/MyWebLog.Data/SQLiteData.fs | 22 +- 9 files changed, 303 insertions(+), 396 deletions(-) diff --git a/src/MyWebLog.Data/SQLite/Helpers.fs b/src/MyWebLog.Data/SQLite/Helpers.fs index e5357bd..7ee8090 100644 --- a/src/MyWebLog.Data/SQLite/Helpers.fs +++ b/src/MyWebLog.Data/SQLite/Helpers.fs @@ -76,13 +76,6 @@ let toList<'T> (it: SqliteDataReader -> 'T) (rdr: SqliteDataReader) = seq { while rdr.Read () do it rdr } |> List.ofSeq -/// Verify that the web log ID matches before returning an item -let verifyWebLog<'T> webLogId (prop : 'T -> WebLogId) (it : SqliteDataReader -> 'T) (rdr : SqliteDataReader) = - if rdr.Read() then - let item = it rdr - if prop item = webLogId then Some item else None - else None - /// Execute a command that returns no data let write (cmd: SqliteCommand) = backgroundTask { let! _ = cmd.ExecuteNonQueryAsync() @@ -90,7 +83,7 @@ let write (cmd: SqliteCommand) = backgroundTask { } /// Add a possibly-missing parameter, substituting null for None -let maybe<'T> (it : 'T option) : obj = match it with Some x -> x :> obj | None -> DBNull.Value +let maybe<'T> (it: 'T option) : obj = match it with Some x -> x :> obj | None -> DBNull.Value /// Create a value for a Duration let durationParam = @@ -261,7 +254,8 @@ let cmdToList<'TDoc> (cmd: SqliteCommand) ser = backgroundTask { } /// Queries to assist with document manipulation -module Query = +[] +module QueryOld = /// Fragment to add an ID condition to a WHERE clause (parameter @id) let whereById = @@ -292,6 +286,14 @@ module Query = $"DELETE FROM %s{table} WHERE {whereById}" +/// Create a document ID parameter +let idParam (key: 'TKey) = + SqliteParameter("@id", string key) + +/// Create a web log ID parameter +let webLogParam (webLogId: WebLogId) = + SqliteParameter("@webLogId", string webLogId) + let addParam (cmd: SqliteCommand) name (value: obj) = cmd.Parameters.AddWithValue(name, value) |> ignore @@ -307,18 +309,39 @@ let addDocParam<'TDoc> (cmd: SqliteCommand) (doc: 'TDoc) ser = let addWebLogId (cmd: SqliteCommand) (webLogId: WebLogId) = addParam cmd "@webLogId" (string webLogId) +open BitBadger.Sqlite.FSharp.Documents +open BitBadger.Sqlite.FSharp.Documents.WithConn + /// Functions for manipulating documents module Document = + /// Queries to assist with document manipulation + module Query = + + /// Fragment to add a web log ID condition to a WHERE clause (parameter @webLogId) + let whereByWebLog = + Query.whereFieldEquals "WebLogId" "@webLogId" + + /// A SELECT query to count documents for a given web log ID + let countByWebLog table = + $"{Query.Count.all table} WHERE {whereByWebLog}" + + /// A query to select from a table by the document's ID and its web log ID + let selectByIdAndWebLog table = + $"{Query.Find.byFieldEquals table} AND {whereByWebLog}" + + /// A query to select from a table by its web log ID + let selectByWebLog table = + $"{Query.selectFromTable table} WHERE {whereByWebLog}" + /// Count documents for the given web log ID - let countByWebLog (conn: SqliteConnection) table webLogId = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- Query.countByWebLog table - addWebLogId cmd webLogId - return! count cmd + let countByWebLog table (webLogId: WebLogId) conn = backgroundTask { + let! count = Count.byFieldEquals table "WebLogId" webLogId conn + return int count } /// Find a document by its ID + [] let findById<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) = backgroundTask { use cmd = conn.CreateCommand() cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById}" @@ -329,55 +352,49 @@ module Document = } /// Find a document by its ID and web log ID - let findByIdAndWebLog<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) webLogId = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById} AND {Query.whereByWebLog}" - addDocId cmd key - addWebLogId cmd webLogId - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - return if isFound then Some (Map.fromDoc<'TDoc> ser rdr) else None - } + let findByIdAndWebLog<'TKey, 'TDoc> table (key: 'TKey) webLogId conn = + Custom.single (Query.selectByIdAndWebLog table) [ idParam key; webLogParam webLogId ] fromData<'TDoc> conn /// Find documents for the given web log - let findByWebLog<'TDoc> (conn: SqliteConnection) ser table webLogId = - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereByWebLog}" - addWebLogId cmd webLogId - cmdToList<'TDoc> cmd ser + let findByWebLog<'TDoc> table (webLogId: WebLogId) conn = + Find.byFieldEquals<'TDoc> table "WebLogId" webLogId conn /// Insert a document + [] let insert<'TDoc> (conn: SqliteConnection) ser table (doc: 'TDoc) = backgroundTask { use cmd = conn.CreateCommand() - cmd.CommandText <- Query.insert table + cmd.CommandText <- QueryOld.insert table addDocParam<'TDoc> cmd doc ser do! write cmd } /// Update (replace) a document by its ID + [] let update<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) (doc: 'TDoc) = backgroundTask { use cmd = conn.CreateCommand() - cmd.CommandText <- Query.updateById table + cmd.CommandText <- QueryOld.updateById table addDocId cmd key addDocParam<'TDoc> cmd doc ser do! write cmd } /// Update a field in a document by its ID + [] let updateField<'TKey, 'TValue> (conn: SqliteConnection) ser table (key: 'TKey) jsonField (value: 'TValue) = backgroundTask { use cmd = conn.CreateCommand() cmd.CommandText <- - $"UPDATE %s{table} SET data = json_set(data, '$.{jsonField}', json(@it)) WHERE {Query.whereById}" + $"UPDATE %s{table} SET data = json_set(data, '$.{jsonField}', json(@it)) WHERE {QueryOld.whereById}" addDocId cmd key addParam cmd "@it" (Utils.serialize ser value) do! write cmd } /// Delete a document by its ID + [] let delete<'TKey> (conn: SqliteConnection) table (key: 'TKey) = backgroundTask { use cmd = conn.CreateCommand() - cmd.CommandText <- Query.deleteById table + cmd.CommandText <- QueryOld.deleteById table addDocId cmd key do! write cmd } @@ -386,29 +403,24 @@ module Document = module Revisions = /// Find all revisions for the given entity - let findByEntityId<'TKey> (conn: SqliteConnection) revTable entityTable (key: 'TKey) = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- + let findByEntityId<'TKey> revTable entityTable (key: 'TKey) conn = + Custom.list $"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC" - addDocId cmd key - use! rdr = cmd.ExecuteReaderAsync() - return toList Map.toRevision rdr - } + [ idParam key ] + Map.toRevision + conn /// Find all revisions for all posts for the given web log - let findByWebLog<'TKey> (conn: SqliteConnection) revTable entityTable (keyFunc: string -> 'TKey) - webLogId = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- + let findByWebLog<'TKey> revTable entityTable (keyFunc: string -> 'TKey) webLogId conn = + Custom.list $"SELECT pr.* FROM %s{revTable} pr INNER JOIN %s{entityTable} p ON p.data ->> 'Id' = pr.{entityTable}_id - WHERE p.{Query.whereByWebLog} + WHERE p.{Document.Query.whereByWebLog} ORDER BY as_of DESC" - addWebLogId cmd webLogId - use! rdr = cmd.ExecuteReaderAsync() - return toList (fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr) rdr - } + [ webLogParam webLogId ] + (fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr) + conn /// Parameters for a revision INSERT statement let revParams<'TKey> (key: 'TKey) rev = @@ -416,26 +428,15 @@ module Revisions = SqliteParameter("@id", string key) SqliteParameter("@text", rev.Text) ] - /// The SQL statement to insert a revision - let insertSql table = - $"INSERT INTO %s{table} VALUES (@id, @asOf, @text)" - /// Update a page or post's revisions - let update<'TKey> (conn: SqliteConnection) revTable entityTable (key: 'TKey) oldRevs newRevs = backgroundTask { + let update<'TKey> revTable entityTable (key: 'TKey) oldRevs newRevs conn = backgroundTask { let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then - use cmd = conn.CreateCommand() - if not (List.isEmpty toDelete) then - cmd.CommandText <- $"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf" - for delRev in toDelete do - cmd.Parameters.Clear() - addDocId cmd key - addParam cmd "@asOf" delRev.AsOf - do! write cmd - if not (List.isEmpty toAdd) then - cmd.CommandText <- insertSql revTable - for addRev in toAdd do - cmd.Parameters.Clear() - cmd.Parameters.AddRange(revParams key addRev) - do! write cmd + for delRev in toDelete do + do! Custom.nonQuery + $"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf" + [ idParam key; SqliteParameter("@asOf", instantParam delRev.AsOf) ] + conn + for addRev in toAdd do + do! Custom.nonQuery $"INSERT INTO {revTable} VALUES (@id, @asOf, @text)" (revParams key addRev) conn } diff --git a/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs b/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs index 3e892c2..33bf01b 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs @@ -1,6 +1,8 @@ namespace MyWebLog.Data.SQLite open System.Threading.Tasks +open BitBadger.Sqlite.FSharp.Documents +open BitBadger.Sqlite.FSharp.Documents.WithConn open Microsoft.Data.Sqlite open Microsoft.Extensions.Logging open MyWebLog @@ -13,29 +15,24 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge /// The name of the parent ID field let parentIdField = nameof Category.Empty.ParentId - /// Add a category - let add (cat: Category) = - log.LogTrace "Category.add" - Document.insert conn ser Table.Category cat - /// Count all categories for the given web log let countAll webLogId = log.LogTrace "Category.countAll" - Document.countByWebLog conn Table.Category webLogId + Document.countByWebLog Table.Category webLogId conn /// Count all top-level categories for the given web log - let countTopLevel webLogId = backgroundTask { + let countTopLevel webLogId = log.LogTrace "Category.countTopLevel" - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL" - addWebLogId cmd webLogId - return! count cmd - } + Custom.scalar + $"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL" + [ webLogParam webLogId ] + (fun rdr -> int (rdr.GetInt64(0))) + conn /// Find all categories for the given web log let findByWebLog webLogId = log.LogTrace "Category.findByWebLog" - Document.findByWebLog conn ser Table.Category webLogId + Document.findByWebLog Table.Category webLogId conn /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { @@ -53,104 +50,74 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge |> Seq.append (Seq.singleton it.Id) |> List.ofSeq |> inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" - use cmd = conn.CreateCommand() - cmd.CommandText <- $" + let query = $""" SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}') FROM {Table.Post} - WHERE {Query.whereByWebLog} - AND data ->> '{nameof Post.Empty.Status}' = '{string Published}' - AND {catSql}" - addWebLogId cmd webLogId - cmd.Parameters.AddRange catParams - let! postCount = count cmd - return it.Id, postCount + WHERE {Document.Query.whereByWebLog} + AND {Query.whereFieldEquals (nameof Post.Empty.Status) $"'{string Published}'"} + AND {catSql}""" + let! postCount = Custom.scalar query (webLogParam webLogId :: catParams) (_.GetInt64(0)) conn + return it.Id, int postCount }) |> Task.WhenAll return ordered |> Seq.map (fun cat -> { cat with - PostCount = - counts - |> Array.tryFind (fun c -> fst c = cat.Id) - |> Option.map snd - |> Option.defaultValue 0 }) + PostCount = defaultArg (counts |> Array.tryFind (fun c -> fst c = cat.Id) |> Option.map snd) 0 + }) |> Array.ofSeq } /// Find a category by its ID for the given web log let findById catId webLogId = log.LogTrace "Category.findById" - Document.findByIdAndWebLog conn ser Table.Category catId webLogId + Document.findByIdAndWebLog Table.Category catId webLogId conn /// Delete a category let delete catId webLogId = backgroundTask { log.LogTrace "Category.delete" match! findById catId webLogId with | Some cat -> - use cmd = conn.CreateCommand() // Reassign any children to the category's parent category - cmd.CommandText <- $"SELECT COUNT(*) FROM {Table.Category} WHERE data ->> '{parentIdField}' = @parentId" - addParam cmd "@parentId" (string catId) - let! children = count cmd + let! children = Count.byFieldEquals Table.Category parentIdField catId conn if children > 0 then - cmd.CommandText <- $" - UPDATE {Table.Category} - SET data = json_set(data, '$.{parentIdField}', @newParentId) - WHERE data ->> '{parentIdField}' = @parentId" - addParam cmd "@newParentId" (maybe (cat.ParentId |> Option.map string)) - do! write cmd + do! Update.partialByFieldEquals Table.Category parentIdField catId {| ParentId = cat.ParentId |} conn // Delete the category off all posts where it is assigned, and the category itself let catIdField = Post.Empty.CategoryIds - cmd.CommandText <- $" - SELECT data ->> '{Post.Empty.Id}' AS id, data -> '{catIdField}' AS cat_ids - FROM {Table.Post} - WHERE {Query.whereByWebLog} - AND EXISTS - (SELECT 1 FROM json_each({Table.Post}.data -> '{catIdField}') WHERE json_each.value = @id)" - cmd.Parameters.Clear() - addDocId cmd catId - addWebLogId cmd webLogId - use! postRdr = cmd.ExecuteReaderAsync() - if postRdr.HasRows then - let postIdAndCats = - toList - (fun rdr -> - Map.getString "id" rdr, Utils.deserialize ser (Map.getString "cat_ids" rdr)) - postRdr - do! postRdr.CloseAsync() - for postId, cats in postIdAndCats do - cmd.CommandText <- $" - UPDATE {Table.Post} - SET data = json_set(data, '$.{catIdField}', json(@catIds)) - WHERE {Query.whereById}" - cmd.Parameters.Clear() - addDocId cmd postId - addParam cmd "@catIds" (cats |> List.filter (fun it -> it <> string catId) |> Utils.serialize ser) - do! write cmd + let! posts = + Custom.list + $"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}' + FROM {Table.Post} + WHERE {Document.Query.whereByWebLog} + AND EXISTS + (SELECT 1 + FROM json_each({Table.Post}.data -> '{catIdField}') + WHERE json_each.value = @id)" + [ idParam catId; webLogParam webLogId ] + (fun rdr -> rdr.GetString(0), Utils.deserialize ser (rdr.GetString(1))) + conn + for postId, cats in posts do + do! Update.partialById + Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |} conn do! Document.delete conn Table.Category catId - return if children = 0 then CategoryDeleted else ReassignedChildCategories + return if children = 0L then CategoryDeleted else ReassignedChildCategories | None -> return CategoryNotFound } + /// Save a category + let save cat = + log.LogTrace "Category.save" + save Table.Category cat conn + /// Restore categories from a backup let restore cats = backgroundTask { - for cat in cats do - do! add cat - } - - /// Update a category - let update (cat: Category) = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.updateById} AND {Query.whereByWebLog}" - addDocId cmd cat.Id - addDocParam cmd cat ser - addWebLogId cmd cat.WebLogId - do! write cmd + log.LogTrace "Category.restore" + for cat in cats do do! save cat } interface ICategoryData with - member _.Add cat = add cat + member _.Add cat = save cat member _.CountAll webLogId = countAll webLogId member _.CountTopLevel webLogId = countTopLevel webLogId member _.FindAllForView webLogId = findAllForView webLogId @@ -158,4 +125,4 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge member _.FindByWebLog webLogId = findByWebLog webLogId member _.Delete catId webLogId = delete catId webLogId member _.Restore cats = restore cats - member _.Update cat = update cat + member _.Update cat = save cat diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs index a97c362..ba62ea0 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs @@ -1,20 +1,21 @@ namespace MyWebLog.Data.SQLite open System.Threading.Tasks +open BitBadger.Sqlite.FSharp.Documents +open BitBadger.Sqlite.FSharp.Documents.WithConn open Microsoft.Data.Sqlite open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data -open Newtonsoft.Json /// SQLite myWebLog page data implementation -type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = +type SQLitePageData(conn: SqliteConnection, log: ILogger) = - /// The JSON field for the permalink - let linkField = $"data ->> '{nameof Page.Empty.Permalink}'" + /// The JSON field name for the permalink + let linkName = nameof Page.Empty.Permalink - /// The JSON field for the "is in page list" flag - let pgListField = $"data ->> '{nameof Page.Empty.IsInPageList}'" + /// The JSON field name for the "is in page list" flag + let pgListName = nameof Page.Empty.IsInPageList /// The JSON field for the title of the page let titleField = $"data ->> '{nameof Page.Empty.Title}'" @@ -24,57 +25,44 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { log.LogTrace "Page.appendPageRevisions" - let! revisions = Revisions.findByEntityId conn Table.PageRevision Table.Page page.Id + let! revisions = Revisions.findByEntityId Table.PageRevision Table.Page page.Id conn return { page with Revisions = revisions } } - /// Return a page with no text - let withoutText (page: Page) = - { page with Text = "" } - /// Update a page's revisions let updatePageRevisions (pageId: PageId) oldRevs newRevs = log.LogTrace "Page.updatePageRevisions" - Revisions.update conn Table.PageRevision Table.Page pageId oldRevs newRevs + Revisions.update Table.PageRevision Table.Page pageId oldRevs newRevs conn // IMPLEMENTATION FUNCTIONS - /// Add a page - let add page = backgroundTask { - log.LogTrace "Page.add" - do! Document.insert conn ser Table.Page { page with Revisions = [] } - do! updatePageRevisions page.Id [] page.Revisions - } - /// Get all pages for a web log (without text or revisions) - let all webLogId = backgroundTask { + let all webLogId = log.LogTrace "Page.all" - use cmd = conn.CreateCommand() - cmd.CommandText <- - $"{Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} ORDER BY LOWER({titleField})" - addWebLogId cmd webLogId - let! pages = cmdToList cmd ser - return pages |> List.map withoutText - } + Custom.list + $"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})" + [ webLogParam webLogId ] + (fun rdr -> { fromData rdr with Text = "" }) + conn /// Count all pages for the given web log let countAll webLogId = log.LogTrace "Page.countAll" - Document.countByWebLog conn Table.Page webLogId + Document.countByWebLog Table.Page webLogId conn /// Count all pages shown in the page list for the given web log - let countListed webLogId = backgroundTask { + let countListed webLogId = log.LogTrace "Page.countListed" - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.countByWebLog} AND {pgListField} = 'true'" - addWebLogId cmd webLogId - return! count cmd - } + Custom.scalar + $"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals pgListName "'true'"}""" + [ webLogParam webLogId ] + (fun rdr -> int (rdr.GetInt64(0))) + conn /// Find a page by its ID (without revisions) let findById pageId webLogId = log.LogTrace "Page.findById" - Document.findByIdAndWebLog conn ser Table.Page pageId webLogId + Document.findByIdAndWebLog Table.Page pageId webLogId conn /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { @@ -92,93 +80,74 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = log.LogTrace "Page.delete" match! findById pageId webLogId with | Some _ -> - use cmd = conn.CreateCommand() - cmd.CommandText <- $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.deleteById}" - addDocId cmd pageId - do! write cmd + do! Custom.nonQuery + $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}" + [ idParam pageId ] + conn return true | None -> return false } /// Find a page by its permalink for the given web log - let findByPermalink (permalink: Permalink) webLogId = backgroundTask { + let findByPermalink (permalink: Permalink) webLogId = log.LogTrace "Page.findByPermalink" - use cmd = conn.CreateCommand() - cmd.CommandText <- $" {Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} AND {linkField} = @link" - addWebLogId cmd webLogId - addParam cmd "@link" (string permalink) - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - return if isFound then Some (Map.fromDoc ser rdr) else None - } + Custom.single + $"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}""" + [ webLogParam webLogId; SqliteParameter("@link", string permalink) ] + fromData + conn /// Find the current permalink within a set of potential prior permalinks for the given web log - let findCurrentPermalink (permalinks: Permalink list) webLogId = backgroundTask { + let findCurrentPermalink (permalinks: Permalink list) webLogId = log.LogTrace "Page.findCurrentPermalink" let linkSql, linkParams = inJsonArray Table.Page (nameof Page.Empty.PriorPermalinks) "link" permalinks - use cmd = conn.CreateCommand() - cmd.CommandText <- - $"SELECT {linkField} AS permalink FROM {Table.Page} WHERE {Query.whereByWebLog} AND {linkSql}" - addWebLogId cmd webLogId - cmd.Parameters.AddRange linkParams - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - return if isFound then Some (Map.toPermalink rdr) else None - } + Custom.single + $"SELECT data ->> '{linkName}' AS permalink + FROM {Table.Page} + WHERE {Document.Query.whereByWebLog} AND {linkSql}" + (webLogParam webLogId :: linkParams) + Map.toPermalink + conn /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { log.LogTrace "Page.findFullByWebLog" - let! pages = Document.findByWebLog conn ser Table.Page webLogId - let! withRevs = - pages - |> List.map (fun page -> backgroundTask { return! appendPageRevisions page }) - |> Task.WhenAll + let! pages = Document.findByWebLog Table.Page webLogId conn + let! withRevs = pages |> List.map appendPageRevisions |> Task.WhenAll return List.ofArray withRevs } /// Get all listed pages for the given web log (without revisions or text) - let findListed webLogId = backgroundTask { + let findListed webLogId = log.LogTrace "Page.findListed" - use cmd = conn.CreateCommand () - cmd.CommandText <- $" - {Query.selectFromTable Table.Page} - WHERE {Query.whereByWebLog} - AND {pgListField} = 'true' - ORDER BY LOWER({titleField})" - addWebLogId cmd webLogId - let! pages = cmdToList cmd ser - return pages |> List.map withoutText - } + Custom.list + $"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereFieldEquals pgListName "'true'"} + ORDER BY LOWER({titleField})""" + [ webLogParam webLogId ] + (fun rdr -> { fromData rdr with Text = "" }) + conn /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = log.LogTrace "Page.findPageOfPages" - use cmd = conn.CreateCommand() - cmd.CommandText <- $" - {Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} - ORDER BY LOWER({titleField}) - LIMIT @pageSize OFFSET @toSkip" - addWebLogId cmd webLogId - addParam cmd "@pageSize" 26 - addParam cmd "@toSkip" ((pageNbr - 1) * 25) - cmdToList cmd ser + Custom.list + $"{Document.Query.selectByWebLog Table.Page} ORDER BY LOWER({titleField}) LIMIT @pageSize OFFSET @toSkip" + [ webLogParam webLogId; SqliteParameter("@pageSize", 26); SqliteParameter("@toSkip", (pageNbr - 1) * 25) ] + fromData + conn + + /// Save a page + let save (page: Page) = backgroundTask { + log.LogTrace "Page.update" + let! oldPage = findFullById page.Id page.WebLogId + do! save Table.Page { page with Revisions = [] } conn + do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions + } /// Restore pages from a backup let restore pages = backgroundTask { log.LogTrace "Page.restore" - for page in pages do - do! add page - } - - /// Update a page - let update (page: Page) = backgroundTask { - log.LogTrace "Page.update" - match! findFullById page.Id page.WebLogId with - | Some oldPage -> - do! Document.update conn ser Table.Page page.Id { page with Revisions = [] } - do! updatePageRevisions page.Id oldPage.Revisions page.Revisions - | None -> () + for page in pages do do! save page } /// Update a page's prior permalinks @@ -186,13 +155,13 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = log.LogTrace "Page.updatePriorPermalinks" match! findById pageId webLogId with | Some _ -> - do! Document.updateField conn ser Table.Page pageId (nameof Page.Empty.PriorPermalinks) permalinks + do! Update.partialById Table.Page pageId {| PriorPermalinks = permalinks |} conn return true - | None -> return false + | None -> return false } interface IPageData with - member _.Add page = add page + member _.Add page = save page member _.All webLogId = all webLogId member _.CountAll webLogId = countAll webLogId member _.CountListed webLogId = countListed webLogId @@ -205,5 +174,5 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = member _.FindListed webLogId = findListed webLogId member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr member _.Restore pages = restore pages - member _.Update page = update page + member _.Update page = save page member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks diff --git a/src/MyWebLog.Data/SQLite/SQLitePostData.fs b/src/MyWebLog.Data/SQLite/SQLitePostData.fs index b6f82a7..0792fdf 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePostData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePostData.fs @@ -1,84 +1,70 @@ namespace MyWebLog.Data.SQLite open System.Threading.Tasks +open BitBadger.Sqlite.FSharp.Documents +open BitBadger.Sqlite.FSharp.Documents.WithConn open Microsoft.Data.Sqlite open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open NodaTime /// SQLite myWebLog post data implementation -type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = +type SQLitePostData(conn: SqliteConnection, log: ILogger) = - /// The JSON field for the post's permalink - let linkField = $"data ->> '{nameof Post.Empty.Permalink}'" + /// The name of the JSON field for the post's permalink + let linkName = nameof Post.Empty.Permalink /// The JSON field for when the post was published let publishField = $"data ->> '{nameof Post.Empty.PublishedOn}'" - /// The JSON field for post status - let statField = $"data ->> '{nameof Post.Empty.Status}'" + /// The name of the JSON field for the post's status + let statName = nameof Post.Empty.Status // SUPPORT FUNCTIONS /// Append revisions to a post let appendPostRevisions (post: Post) = backgroundTask { log.LogTrace "Post.appendPostRevisions" - let! revisions = Revisions.findByEntityId conn Table.PostRevision Table.Post post.Id + let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id conn return { post with Revisions = revisions } } /// The SELECT statement to retrieve posts with a web log ID parameter - let postByWebLog = $"{Query.selectFromTable Table.Post} WHERE {Query.whereByWebLog}" + let postByWebLog = Document.Query.selectByWebLog Table.Post /// The SELECT statement to retrieve published posts with a web log ID parameter - let publishedPostByWebLog = $"{postByWebLog} AND {statField} = '{string Published}'" - - /// Remove the text from a post - let withoutText (post: Post) = - { post with Text = "" } + let publishedPostByWebLog = $"""{postByWebLog} AND {Query.whereFieldEquals statName $"'{string Published}'"}""" /// Update a post's revisions let updatePostRevisions (postId: PostId) oldRevs newRevs = log.LogTrace "Post.updatePostRevisions" - Revisions.update conn Table.PostRevision Table.Post postId oldRevs newRevs + Revisions.update Table.PostRevision Table.Post postId oldRevs newRevs conn // IMPLEMENTATION FUNCTIONS - /// Add a post - let add (post: Post) = backgroundTask { - log.LogTrace "Post.add" - do! Document.insert conn ser Table.Post { post with Revisions = [] } - do! updatePostRevisions post.Id [] post.Revisions - } - /// Count posts in a status for the given web log - let countByStatus (status: PostStatus) webLogId = backgroundTask { + let countByStatus (status: PostStatus) webLogId = log.LogTrace "Post.countByStatus" - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.countByWebLog Table.Post} AND {statField} = @status" - addWebLogId cmd webLogId - addParam cmd "@status" (string status) - return! count cmd - } + Custom.scalar + $"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals statName "@status"}""" + [ webLogParam webLogId; SqliteParameter("@status", string status) ] + (fun rdr -> int (rdr.GetInt64(0))) + conn - /// Find a post by its ID for the given web log (excluding revisions and prior permalinks + /// Find a post by its ID for the given web log (excluding revisions) let findById postId webLogId = log.LogTrace "Post.findById" - Document.findByIdAndWebLog conn ser Table.Post postId webLogId + Document.findByIdAndWebLog Table.Post postId webLogId conn - /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks) - let findByPermalink (permalink: Permalink) webLogId = backgroundTask { + /// Find a post by its permalink for the given web log (excluding revisions) + let findByPermalink (permalink: Permalink) webLogId = log.LogTrace "Post.findByPermalink" - use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.selectFromTable Table.Post} WHERE {Query.whereByWebLog} AND {linkField} = @link" - addWebLogId cmd webLogId - addParam cmd "@link" (string permalink) - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - return if isFound then Some (Map.fromDoc ser rdr) else None - } + Custom.single + $"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}""" + [ webLogParam webLogId; SqliteParameter("@link", string permalink) ] + fromData + conn /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { @@ -95,39 +81,34 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = log.LogTrace "Post.delete" match! findById postId webLogId with | Some _ -> - use cmd = conn.CreateCommand() - cmd.CommandText <- $" - DELETE FROM {Table.PostRevision} WHERE post_id = @id; - DELETE FROM {Table.PostComment} WHERE data ->> '{nameof Comment.Empty.PostId}' = @id; - DELETE FROM {Table.Post} WHERE {Query.whereById}" - addDocId cmd postId - do! write cmd + do! Custom.nonQuery + $"""DELETE FROM {Table.PostRevision} WHERE post_id = @id; + DELETE FROM {Table.PostComment} + WHERE {Query.whereFieldEquals (nameof Comment.Empty.PostId) "@id"}; + {Query.Delete.byId Table.Post}""" + [ idParam postId ] + conn return true | None -> return false } /// Find the current permalink from a list of potential prior permalinks for the given web log - let findCurrentPermalink (permalinks: Permalink list) webLogId = backgroundTask { + let findCurrentPermalink (permalinks: Permalink list) webLogId = log.LogTrace "Post.findCurrentPermalink" let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks - use cmd = conn.CreateCommand() - cmd.CommandText <- - $"SELECT {linkField} AS permalink FROM {Table.Post} WHERE {Query.whereByWebLog} AND {linkSql}" - addWebLogId cmd webLogId - cmd.Parameters.AddRange linkParams - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - return if isFound then Some (Map.toPermalink rdr) else None - } + Custom.single + $"SELECT data ->> '{linkName}' + FROM {Table.Post} + WHERE {Document.Query.whereByWebLog} AND {linkSql}" + (webLogParam webLogId :: linkParams) + Map.toPermalink + conn /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { log.LogTrace "Post.findFullByWebLog" - let! posts = Document.findByWebLog conn ser Table.Post webLogId - let! withRevs = - posts - |> List.map (fun post -> backgroundTask { return! appendPostRevisions post }) - |> Task.WhenAll + let! posts = Document.findByWebLog Table.Post webLogId conn + let! withRevs = posts |> List.map appendPostRevisions |> Task.WhenAll return List.ofArray withRevs } @@ -135,102 +116,91 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = let findPageOfCategorizedPosts webLogId (categoryIds: CategoryId list) pageNbr postsPerPage = log.LogTrace "Post.findPageOfCategorizedPosts" let catSql, catParams = inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" categoryIds - use cmd = conn.CreateCommand () - cmd.CommandText <- $" - {publishedPostByWebLog} AND {catSql} - ORDER BY {publishField} DESC - LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - addWebLogId cmd webLogId - cmd.Parameters.AddRange catParams - cmdToList cmd ser + Custom.list + $"{publishedPostByWebLog} AND {catSql} + ORDER BY {publishField} DESC + LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" + (webLogParam webLogId :: catParams) + fromData + conn - /// Get a page of posts for the given web log (excludes revisions) - let findPageOfPosts webLogId pageNbr postsPerPage = backgroundTask { + /// Get a page of posts for the given web log (excludes text and revisions) + let findPageOfPosts webLogId pageNbr postsPerPage = log.LogTrace "Post.findPageOfPosts" - use cmd = conn.CreateCommand() - cmd.CommandText <- $" - {postByWebLog} - ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}' - LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - addWebLogId cmd webLogId - let! posts = cmdToList cmd ser - return posts |> List.map withoutText - } + Custom.list + $"{postByWebLog} + ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}' + LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" + [ webLogParam webLogId ] + (fun rdr -> { fromData rdr with Text = "" }) + conn /// Get a page of published posts for the given web log (excludes revisions) let findPageOfPublishedPosts webLogId pageNbr postsPerPage = log.LogTrace "Post.findPageOfPublishedPosts" - use cmd = conn.CreateCommand() - cmd.CommandText <- $" - {publishedPostByWebLog} - ORDER BY {publishField} DESC - LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - addWebLogId cmd webLogId - cmdToList cmd ser + Custom.list + $"{publishedPostByWebLog} + ORDER BY {publishField} DESC + LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" + [ webLogParam webLogId ] + fromData + conn /// Get a page of tagged posts for the given web log (excludes revisions) let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = log.LogTrace "Post.findPageOfTaggedPosts" let tagSql, tagParams = inJsonArray Table.Post (nameof Post.Empty.Tags) "tag" [ tag ] - use cmd = conn.CreateCommand() - cmd.CommandText <- $" - {publishedPostByWebLog} AND {tagSql} - ORDER BY p.published_on DESC - LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - addWebLogId cmd webLogId - cmd.Parameters.AddRange tagParams - cmdToList cmd ser + Custom.list + $"{publishedPostByWebLog} AND {tagSql} + ORDER BY p.published_on DESC + LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" + (webLogParam webLogId :: tagParams) + fromData + conn /// Find the next newest and oldest post from a publish date for the given web log let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask { log.LogTrace "Post.findSurroundingPosts" - use cmd = conn.CreateCommand () - addWebLogId cmd webLogId - addParam cmd "@publishedOn" (instantParam publishedOn) - - cmd.CommandText <- - $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" - use! rdr = cmd.ExecuteReaderAsync() - let! isFound = rdr.ReadAsync() - let older = if isFound then Some (Map.fromDoc ser rdr) else None - do! rdr.CloseAsync () - - cmd.CommandText <- - $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" - use! rdr = cmd.ExecuteReaderAsync () - let! isFound = rdr.ReadAsync() - let newer = if isFound then Some (Map.fromDoc ser rdr) else None - + let! older = + Custom.single + $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" + [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] + fromData + conn + let! newer = + Custom.single + $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" + [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] + fromData + conn return older, newer } + /// Save a post + let save (post: Post) = backgroundTask { + log.LogTrace "Post.save" + let! oldPost = findFullById post.Id post.WebLogId + do! save Table.Post { post with Revisions = [] } conn + do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions + } + /// Restore posts from a backup let restore posts = backgroundTask { log.LogTrace "Post.restore" - for post in posts do - do! add post - } - - /// Update a post - let update (post: Post) = backgroundTask { - match! findFullById post.Id post.WebLogId with - | Some oldPost -> - do! Document.update conn ser Table.Post post.Id { post with Revisions = [] } - do! updatePostRevisions post.Id oldPost.Revisions post.Revisions - | None -> return () + for post in posts do do! save post } /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId (permalinks: Permalink list) = backgroundTask { match! findById postId webLogId with | Some _ -> - do! Document.updateField conn ser Table.Post postId (nameof Post.Empty.PriorPermalinks) permalinks + do! Update.partialById Table.Post postId {| PriorPermalinks = permalinks |} conn return true - | None -> return false + | None -> return false } interface IPostData with - member _.Add post = add post + member _.Add post = save post member _.CountByStatus status webLogId = countByStatus status webLogId member _.Delete postId webLogId = delete postId webLogId member _.FindById postId webLogId = findById postId webLogId @@ -247,5 +217,5 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = findPageOfTaggedPosts webLogId tag pageNbr postsPerPage member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.Restore posts = restore posts - member _.Update post = update post + member _.Update post = save post member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks diff --git a/src/MyWebLog.Data/SQLite/SQLiteTagMapData.fs b/src/MyWebLog.Data/SQLite/SQLiteTagMapData.fs index 64440ac..9bc3616 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteTagMapData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteTagMapData.fs @@ -29,8 +29,8 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) log.LogTrace "TagMap.findByUrlValue" use cmd = conn.CreateCommand() cmd.CommandText <- $" - {Query.selectFromTable Table.TagMap} - WHERE {Query.whereByWebLog} + {QueryOld.selectFromTable Table.TagMap} + WHERE {QueryOld.whereByWebLog} AND data ->> '{nameof TagMap.Empty.UrlValue}' = @urlValue" addWebLogId cmd webLogId addParam cmd "@urlValue" urlValue @@ -49,7 +49,7 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) log.LogTrace "TagMap.findMappingForTags" use cmd = conn.CreateCommand () let mapSql, mapParams = inClause $"AND data ->> '{nameof TagMap.Empty.Tag}'" "tag" id tags - cmd.CommandText <- $"{Query.selectFromTable Table.TagMap} WHERE {Query.whereByWebLog} {mapSql}" + cmd.CommandText <- $"{QueryOld.selectFromTable Table.TagMap} WHERE {QueryOld.whereByWebLog} {mapSql}" addWebLogId cmd webLogId cmd.Parameters.AddRange mapParams cmdToList cmd ser diff --git a/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs b/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs index 5d78177..151496e 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs @@ -20,7 +20,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger) let all () = backgroundTask { log.LogTrace "Theme.all" use cmd = conn.CreateCommand() - cmd.CommandText <- $"{Query.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}" + cmd.CommandText <- $"{QueryOld.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}" let! themes = cmdToList cmd ser return themes |> List.map withoutTemplateText } @@ -55,7 +55,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger) use cmd = conn.CreateCommand() cmd.CommandText <- $" DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id; - DELETE FROM {Table.Theme} WHERE {Query.whereById}" + DELETE FROM {Table.Theme} WHERE {QueryOld.whereById}" addDocId cmd themeId do! write cmd return true diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs index ebd7925..7cec1a7 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs @@ -19,7 +19,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) let all () = log.LogTrace "WebLog.all" use cmd = conn.CreateCommand() - cmd.CommandText <- Query.selectFromTable Table.WebLog + cmd.CommandText <- QueryOld.selectFromTable Table.WebLog cmdToList cmd ser /// Delete a web log by its ID @@ -48,7 +48,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) log.LogTrace "WebLog.findByHost" use cmd = conn.CreateCommand() cmd.CommandText <- - $"{Query.selectFromTable Table.WebLog} WHERE data ->> '{nameof WebLog.Empty.UrlBase}' = @urlBase" + $"{QueryOld.selectFromTable Table.WebLog} WHERE data ->> '{nameof WebLog.Empty.UrlBase}' = @urlBase" addParam cmd "@urlBase" url use! rdr = cmd.ExecuteReaderAsync() let! isFound = rdr.ReadAsync() diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs index f37de25..5029f98 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs @@ -62,8 +62,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog log.LogTrace "WebLogUser.findByEmail" use cmd = conn.CreateCommand() cmd.CommandText <- $" - {Query.selectFromTable Table.WebLogUser} - WHERE {Query.whereByWebLog} + {QueryOld.selectFromTable Table.WebLogUser} + WHERE {QueryOld.whereByWebLog} AND data ->> '{nameof WebLogUser.Empty.Email}' = @email" addWebLogId cmd webLogId addParam cmd "@email" email @@ -84,7 +84,7 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog log.LogTrace "WebLogUser.findNames" use cmd = conn.CreateCommand() let nameSql, nameParams = inClause "AND data ->> 'Id'" "id" string userIds - cmd.CommandText <- $"{Query.selectFromTable Table.WebLogUser} WHERE {Query.whereByWebLog} {nameSql}" + cmd.CommandText <- $"{QueryOld.selectFromTable Table.WebLogUser} WHERE {QueryOld.whereByWebLog} {nameSql}" addWebLogId cmd webLogId cmd.Parameters.AddRange nameParams let! users = cmdToList cmd ser @@ -105,8 +105,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog cmd.CommandText <- $" UPDATE {Table.WebLogUser} SET data = json_set(data, '$.{nameof WebLogUser.Empty.LastSeenOn}', @lastSeenOn) - WHERE {Query.whereById} - AND {Query.whereByWebLog}" + WHERE {QueryOld.whereById} + AND {QueryOld.whereByWebLog}" addDocId cmd userId addWebLogId cmd webLogId addParam cmd "@lastSeenOn" (instantParam (Noda.now ())) diff --git a/src/MyWebLog.Data/SQLiteData.fs b/src/MyWebLog.Data/SQLiteData.fs index ec631bb..b5a85ff 100644 --- a/src/MyWebLog.Data/SQLiteData.fs +++ b/src/MyWebLog.Data/SQLiteData.fs @@ -2,6 +2,7 @@ namespace MyWebLog.Data open System.Threading.Tasks open BitBadger.Sqlite.FSharp.Documents +open BitBadger.Sqlite.FSharp.Documents.WithConn open Microsoft.Data.Sqlite open Microsoft.Extensions.Logging open MyWebLog @@ -12,9 +13,10 @@ open NodaTime /// SQLite myWebLog data implementation type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSerializer) = + /// Create tables (and their associated indexes) if they do not exist let ensureTables () = backgroundTask { - let! tables = Custom.list "SELECT name FROM sqlite_master WHERE type = 'table'" None _.GetString(0) + let! tables = Custom.list "SELECT name FROM sqlite_master WHERE type = 'table'" [] (_.GetString(0)) conn let needsTable table = not (List.contains table tables) @@ -102,19 +104,16 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria } |> Seq.map (fun sql -> log.LogInformation $"""Creating {(sql.Replace("IF NOT EXISTS ", "").Split ' ')[2]} table...""" - Custom.nonQuery sql None) + Custom.nonQuery sql [] conn) let! _ = Task.WhenAll tasks () } /// Set the database version to the specified version - let setDbVersion version = backgroundTask { - use cmd = conn.CreateCommand() - cmd.CommandText <- $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" - do! write cmd - } - + let setDbVersion version = + Custom.nonQuery $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" [] conn + /// Implement the changes between v2-rc1 and v2-rc2 let migrateV2Rc1ToV2Rc2 () = backgroundTask { let logStep = Utils.logMigrationStep log "v2-rc1 to v2-rc2" @@ -418,6 +417,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria /// Migrate from v2 to v2.1 let migrateV2ToV2point1 () = backgroundTask { + // FIXME: This will be a backup/restore scenario, as we're changing to documents for most tables Utils.logMigrationStep log "v2 to v2.1" "Adding redirect rules to web_log table" use cmd = conn.CreateCommand() cmd.CommandText <- "ALTER TABLE web_log ADD COLUMN redirect_rules TEXT NOT NULL DEFAULT '[]'" @@ -454,8 +454,8 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria interface IData with member _.Category = SQLiteCategoryData (conn, ser, log) - member _.Page = SQLitePageData (conn, ser, log) - member _.Post = SQLitePostData (conn, ser, log) + member _.Page = SQLitePageData (conn, log) + member _.Post = SQLitePostData (conn, log) member _.TagMap = SQLiteTagMapData (conn, ser, log) member _.Theme = SQLiteThemeData (conn, ser, log) member _.ThemeAsset = SQLiteThemeAssetData (conn, log) @@ -467,6 +467,6 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria member _.StartUp () = backgroundTask { do! ensureTables () - let! version = Custom.single $"SELECT id FROM {Table.DbVersion}" None _.GetString(0) + let! version = Custom.single $"SELECT id FROM {Table.DbVersion}" [] (_.GetString(0)) conn do! migrate version }