WIP on SQLite doc library

This commit is contained in:
Daniel J. Summers 2023-12-19 18:30:34 -05:00
parent d330c97d9f
commit bb4252f3c1
9 changed files with 303 additions and 396 deletions

View File

@ -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 =
[<Obsolete("change me")>]
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
[<Obsolete("replace this")>]
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
[<Obsolete("replace this")>]
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
[<Obsolete("replace this")>]
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
[<Obsolete("replace this")>]
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
[<Obsolete("replace this")>]
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
}

View File

@ -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<Category> conn ser Table.Category webLogId
Document.findByWebLog<Category> 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<CategoryId, Category> conn ser Table.Category catId webLogId
Document.findByIdAndWebLog<CategoryId, Category> 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<string list> 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<string list> 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<Category> 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

View File

@ -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<Page> 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<Page> 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<Page> 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<PageId, Page> conn ser Table.Page pageId webLogId
Document.findByIdAndWebLog<PageId, Page> 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<Page> ser rdr) else None
}
Custom.single
$"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}"""
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
fromData<Page>
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<Page> conn ser Table.Page webLogId
let! withRevs =
pages
|> List.map (fun page -> backgroundTask { return! appendPageRevisions page })
|> Task.WhenAll
let! pages = Document.findByWebLog<Page> 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<Page> 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<Page> 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<Page> 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<Page>
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

View File

@ -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<PostId, Post> conn ser Table.Post postId webLogId
Document.findByIdAndWebLog<PostId, Post> 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<Post> ser rdr) else None
}
Custom.single
$"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}"""
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
fromData<Post>
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<Post> conn ser Table.Post webLogId
let! withRevs =
posts
|> List.map (fun post -> backgroundTask { return! appendPostRevisions post })
|> Task.WhenAll
let! posts = Document.findByWebLog<Post> 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<Post> cmd ser
Custom.list
$"{publishedPostByWebLog} AND {catSql}
ORDER BY {publishField} DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
(webLogParam webLogId :: catParams)
fromData<Post>
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<Post> 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<Post> 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<Post> cmd ser
Custom.list
$"{publishedPostByWebLog}
ORDER BY {publishField} DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
[ webLogParam webLogId ]
fromData<Post>
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<Post> cmd ser
Custom.list
$"{publishedPostByWebLog} AND {tagSql}
ORDER BY p.published_on DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
(webLogParam webLogId :: tagParams)
fromData<Post>
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<Post> 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<Post> 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<Post>
conn
let! newer =
Custom.single
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
fromData<Post>
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

View File

@ -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<TagMap> cmd ser

View File

@ -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<Theme> 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

View File

@ -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<WebLog> 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()

View File

@ -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<WebLogUser> 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 ()))

View File

@ -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<SQLiteData>, ser: JsonSerializer) =
/// Create tables (and their associated indexes) if they do not exist
let ensureTables () = backgroundTask {
let! tables = Custom.list<string> "SELECT name FROM sqlite_master WHERE type = 'table'" None _.GetString(0)
let! tables = Custom.list<string> "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<SQLiteData>, 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<SQLiteData>, 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<SQLiteData>, 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<SQLiteData>, ser: JsonSeria
member _.StartUp () = backgroundTask {
do! ensureTables ()
let! version = Custom.single<string> $"SELECT id FROM {Table.DbVersion}" None _.GetString(0)
let! version = Custom.single<string> $"SELECT id FROM {Table.DbVersion}" [] (_.GetString(0)) conn
do! migrate version
}