From 2fc2714287534deac12f7fbea4174d40eb922c95 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 18 Jun 2022 12:18:58 -0400 Subject: [PATCH] WIP on SQLite data - Add table definitions - Convert categories - WIP on pages --- src/MyWebLog.Data/LiteDbData.fs | 2 +- src/MyWebLog.Data/SQLiteData.fs | 649 +++++++++++++++++++++++++++----- 2 files changed, 548 insertions(+), 103 deletions(-) diff --git a/src/MyWebLog.Data/LiteDbData.fs b/src/MyWebLog.Data/LiteDbData.fs index 9b647f5..f1b8182 100644 --- a/src/MyWebLog.Data/LiteDbData.fs +++ b/src/MyWebLog.Data/LiteDbData.fs @@ -1,4 +1,4 @@ -namespace MyWebLog.Data +namespace MyWebLog.Dataa open LiteDB open MyWebLog diff --git a/src/MyWebLog.Data/SQLiteData.fs b/src/MyWebLog.Data/SQLiteData.fs index e3ef7ca..3aedc38 100644 --- a/src/MyWebLog.Data/SQLiteData.fs +++ b/src/MyWebLog.Data/SQLiteData.fs @@ -6,9 +6,94 @@ open Microsoft.Data.Sqlite open MyWebLog open MyWebLog.ViewModels -type SQLiteData (conn : SqliteConnection) = +[] +module private SqliteHelpers = + do () - member _.x = "" + /// Run a command that returns a count + let count (cmd : SqliteCommand) = backgroundTask { + let! it = cmd.ExecuteScalarAsync () + return it :?> int + } + + /// Create a list of items from the given data reader + 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 () + () + } + + /// Functions to map domain items from a data reader + module Map = + + /// Get a boolean value from a data reader + let getBoolean (rdr : SqliteDataReader) col = rdr.GetBoolean (rdr.GetOrdinal col) + + /// Get a date/time value from a data reader + let getDateTime (rdr : SqliteDataReader) col = rdr.GetDateTime (rdr.GetOrdinal col) + + /// Get a string value from a data reader + let getString (rdr : SqliteDataReader) col = rdr.GetString (rdr.GetOrdinal col) + + /// Get a possibly null string value from a data reader + let tryString (rdr : SqliteDataReader) col = + if rdr.IsDBNull (rdr.GetOrdinal col) then None else Some (getString rdr col) + + /// Create a category from the current row in the given data reader + let toCategory (rdr : SqliteDataReader) : Category = + { id = CategoryId (getString rdr "id") + webLogId = WebLogId (getString rdr "web_log_id") + name = getString rdr "name" + slug = getString rdr "slug" + description = tryString rdr "description" + parentId = tryString rdr "parent_id" |> Option.map CategoryId + } + + /// Create a meta item from the current row in the given data reader + let toMetaItem (rdr : SqliteDataReader) : MetaItem = + { name = getString rdr "name" + value = getString rdr "value" + } + + /// Create a permalink from the current row in the given data reader + let toPermalink (rdr : SqliteDataReader) : Permalink = + Permalink (getString rdr "permalink") + + /// Create a page from the current row in the given data reader + let toPage (rdr : SqliteDataReader) : Page = + { Page.empty with + id = PageId (getString rdr "id") + webLogId = WebLogId (getString rdr "web_log_id") + authorId = WebLogUserId (getString rdr "author_id") + title = getString rdr "title" + permalink = toPermalink rdr + publishedOn = getDateTime rdr "published_on" + updatedOn = getDateTime rdr "updated_on" + showInPageList = getBoolean rdr "show_in_page_list" + template = tryString rdr "template" + text = getString rdr "page_text" + } + + /// Create a revision from the current row in the given data reader + let toRevision (rdr : SqliteDataReader) : Revision = + { asOf = getDateTime rdr "as_of" + text = MarkupText.parse (getString rdr "revision_text") + } + + +type SQLiteData (conn : SqliteConnection) = // /// Shorthand for accessing the collections in the LiteDB database // let Collection = {| @@ -23,6 +108,45 @@ type SQLiteData (conn : SqliteConnection) = // WebLogUser = db.GetCollection "WebLogUser" // |} + /// Add parameters for category INSERT or UPDATE statements + let addCategoryParameters (cmd : SqliteCommand) (cat : Category) = + [ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.webLogId) + cmd.Parameters.AddWithValue ("@name", cat.name) + cmd.Parameters.AddWithValue ("@slug", cat.slug) + cmd.Parameters.AddWithValue ("@description", + match cat.description with Some d -> d :> obj | None -> DBNull.Value) + cmd.Parameters.AddWithValue ("@parentId", + match cat.parentId with Some (CategoryId parentId) -> parentId :> obj | None -> DBNull.Value) + ] |> ignore + + /// Add parameters for page INSERT or UPDATE statements + let addPageParameters (cmd : SqliteCommand) (page : Page) = + [ cmd.Parameters.AddWithValue ("@id", PageId.toString page.id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.webLogId) + cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.authorId) + cmd.Parameters.AddWithValue ("@title", page.title) + cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.permalink) + cmd.Parameters.AddWithValue ("@publishedOn", page.publishedOn) + cmd.Parameters.AddWithValue ("@updatedOn", page.updatedOn) + cmd.Parameters.AddWithValue ("@showInPageList", page.showInPageList) + cmd.Parameters.AddWithValue ("@template", + match page.template with Some t -> t :> obj | None -> DBNull.Value) + cmd.Parameters.AddWithValue ("@text", page.text) + ] |> ignore + + /// Add a web log ID parameter + let addWebLogId (cmd : SqliteCommand) webLogId = + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore + + /// Append meta items to a page + let appendPageMeta (page : Page) = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT name, value FROM page_meta WHERE page_id = @id" + use! rdr = cmd.ExecuteReaderAsync () + return { page with metadata = toList Map.toMetaItem rdr } + } + /// Return a page with no revisions or prior permalinks let pageWithoutRevisions (page : Page) = { page with revisions = []; priorPermalinks = [] } @@ -43,6 +167,8 @@ type SQLiteData (conn : SqliteConnection) = let postWithoutText post = { postWithoutRevisions post with text = "" } + + /// The connection for this instance member _.Conn = conn @@ -54,59 +180,65 @@ type SQLiteData (conn : SqliteConnection) = member _.add cat = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- - "INSERT INTO Category VALUES (@id, @webLogId, @name, @slug, @description, @parentId)" - [ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.webLogId) - cmd.Parameters.AddWithValue ("@name", cat.name) - cmd.Parameters.AddWithValue ("@slug", cat.slug) - cmd.Parameters.AddWithValue ("@description", - match cat.description with - | Some d -> d :> obj - | None -> DBNull.Value) - cmd.Parameters.AddWithValue ("@parentId", - match cat.parentId with - | Some (CategoryId parentId) -> parentId :> obj - | None -> DBNull.Value) - ] - |> ignore + "INSERT INTO category VALUES (@id, @webLogId, @name, @slug, @description, @parentId)" + addCategoryParameters cmd cat let! _ = cmd.ExecuteNonQueryAsync () () } member _.countAll webLogId = backgroundTask { use cmd = conn.CreateCommand () - cmd.CommandText <- "SELECT COUNT(id) FROM Category WHERE webLogId = @webLpgId" - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore - let! result = cmd.ExecuteScalarAsync () - return result :?> int + cmd.CommandText <- "SELECT COUNT(id) FROM category WHERE web_log_id = @webLogId" + addWebLogId cmd webLogId + return! count cmd } - member _.countTopLevel webLogId = - Collection.Category.Count(fun cat -> cat.webLogId = webLogId && Option.isNone cat.parentId) - |> Task.FromResult + member _.countTopLevel webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- + "SELECT COUNT(id) FROM category WHERE web_log_id = @webLogId AND parent_id IS NULL" + addWebLogId cmd webLogId + return! count cmd + } member _.findAllForView webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM category WHERE web_log_id = @webLogId" + addWebLogId cmd webLogId + use! rdr = cmd.ExecuteReaderAsync () let cats = - Collection.Category.Find (fun cat -> cat.webLogId = webLogId) + seq { + while rdr.Read () do + Map.toCategory rdr + } |> Seq.sortBy (fun cat -> cat.name.ToLowerInvariant ()) |> List.ofSeq - let ordered = orderByHierarchy cats None None [] + if not rdr.IsClosed then do! rdr.CloseAsync () + let ordered = Utils.orderByHierarchy cats None None [] let! counts = ordered |> Seq.map (fun it -> backgroundTask { // Parent category post counts include posts in subcategories - let catIds = - ordered - |> Seq.filter (fun cat -> cat.parentNames |> Array.contains it.name) - |> Seq.map (fun cat -> cat.id :> obj) - |> Seq.append (Seq.singleton it.id) - |> List.ofSeq - let count = - Collection.Post.Count (fun p -> - p.webLogId = webLogId - && p.status = Published - && p.categoryIds |> List.exists (fun cId -> catIds |> List.contains cId)) - return it.id, count + cmd.Parameters.Clear () + addWebLogId cmd webLogId + cmd.CommandText <- + """SELECT COUNT(DISTINCT p.id) + FROM post p + INNER JOIN post_category pc ON pc.post_id = p.id + WHERE p.web_log_id = @webLogId + AND p.status = 'Published' + AND pc.category_id IN (""" + ordered + |> Seq.filter (fun cat -> cat.parentNames |> Array.contains it.name) + |> Seq.map (fun cat -> cat.id) + |> Seq.append (Seq.singleton it.id) + |> Seq.iteri (fun idx item -> + if idx > 0 then cmd.CommandText <- $"{cmd.CommandText}, " + cmd.CommandText <- $"{cmd.CommandText}@catId{idx}" + cmd.Parameters.AddWithValue ($"@catId{idx}", item) |> ignore) + cmd.CommandText <- $"{cmd.CommandText})" + let! postCount = count cmd + return it.id, postCount }) |> Task.WhenAll return @@ -121,38 +253,65 @@ type SQLiteData (conn : SqliteConnection) = |> Array.ofSeq } - member _.findById catId webLogId = - Collection.Category.FindById (CategoryIdMapping.toBson catId) - |> verifyWebLog webLogId (fun c -> c.webLogId) + member _.findById catId webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM category WHERE id = @id" + cmd.Parameters.AddWithValue ("@id", CategoryId.toString catId) |> ignore + use! rdr = cmd.ExecuteReaderAsync () + return verifyWebLog webLogId (fun c -> c.webLogId) Map.toCategory rdr + } - member _.findByWebLog webLogId = - Collection.Category.Find (fun c -> c.webLogId = webLogId) - |> toList + member _.findByWebLog webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM category WHERE web_log_id = @webLogId" + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore + use! rdr = cmd.ExecuteReaderAsync () + return toList Map.toCategory rdr + } member this.delete catId webLogId = backgroundTask { match! this.findById catId webLogId with | Some _ -> + use cmd = conn.CreateCommand () // Delete the category off all posts where it is assigned - Collection.Post.Find (fun p -> p.webLogId = webLogId && p.categoryIds |> List.contains catId) - |> Seq.map (fun p -> - { p with categoryIds = p.categoryIds |> List.filter (fun cId -> cId <> catId) }) - |> Collection.Post.Update - |> ignore + cmd.CommandText <- + """DELETE FROM post_category + WHERE category_id = @id + AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId)""" + let catIdParameter = cmd.Parameters.AddWithValue ("@id", CategoryId.toString catId) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore + do! write cmd // Delete the category itself - let _ = Collection.Category.Delete (CategoryIdMapping.toBson catId) - do! checkpoint () + cmd.CommandText <- "DELETE FROM category WHERE id = @id" + cmd.Parameters.Clear () + cmd.Parameters.Add catIdParameter |> ignore + do! write cmd return true | None -> return false } member _.restore cats = backgroundTask { - let _ = Collection.Category.InsertBulk cats - do! checkpoint () + use cmd = conn.CreateCommand () + cmd.CommandText <- + "INSERT INTO category VALUES (@id, @webLogId, @name, @slug, @description, @parentId)" + for cat in cats do + cmd.Parameters.Clear () + addCategoryParameters cmd cat + do! write cmd } member _.update cat = backgroundTask { - let _ = Collection.Category.Update cat - do! checkpoint () + use cmd = conn.CreateCommand () + cmd.CommandText <- + """UPDATE category + SET name = @name, + slug = @slug, + description = @description, + parent_id = @parentId + WHERE id = @id + AND web_log_id = @webLogId""" + addCategoryParameters cmd cat + do! write cmd } } @@ -160,56 +319,135 @@ type SQLiteData (conn : SqliteConnection) = new IPageData with member _.add page = backgroundTask { - let _ = Collection.Page.Insert page - do! checkpoint () + use cmd = conn.CreateCommand () + // The page itself + cmd.CommandText <- + """INSERT INTO page + VALUES (@id, @webLogId, @authorId, @title, @permalink, @publishedOn, @updatedOn, + @showInPageList, @template, @text)""" + addPageParameters cmd page + do! write cmd + // Metadata + cmd.CommandText <- "INSERT INTO page_meta VALUES (@pageId, @name, @value)" + for meta in page.metadata do + cmd.Parameters.Clear () + [ cmd.Parameters.AddWithValue ("@pageId", PageId.toString page.id) + cmd.Parameters.AddWithValue ("@name", meta.name) + cmd.Parameters.AddWithValue ("@value", meta.value) + ] |> ignore + do! write cmd + // Revisions + cmd.CommandText <- "INSERT INTO page_revision VALUES (@pageId, @asOf, @text)" + for rev in page.revisions do + cmd.Parameters.Clear () + [ cmd.Parameters.AddWithValue ("@pageId", PageId.toString page.id) + cmd.Parameters.AddWithValue ("@asOf", rev.asOf) + cmd.Parameters.AddWithValue ("@text", MarkupText.toString rev.text) + ] |> ignore + do! write cmd } - member _.all webLogId = - Collection.Page.Find (fun p -> p.webLogId = webLogId) - |> Seq.map pageWithoutText - |> Seq.sortBy pageSort - |> toList + member _.all webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM page WHERE web_log_id = @webLogId ORDER BY LOWER(title)" + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore + let noText rdr = { Map.toPage rdr with text = "" } + use! rdr = cmd.ExecuteReaderAsync () + return toList noText rdr + } - member _.countAll webLogId = - Collection.Page.Count (fun p -> p.webLogId = webLogId) - |> Task.FromResult - - member _.countListed webLogId = - Collection.Page.Count (fun p -> p.webLogId = webLogId && p.showInPageList) - |> Task.FromResult - - member _.findFullById pageId webLogId = - Collection.Page.FindById (PageIdMapping.toBson pageId) - |> verifyWebLog webLogId (fun it -> it.webLogId) - - member this.findById pageId webLogId = backgroundTask { - let! page = this.findFullById pageId webLogId - return page |> Option.map pageWithoutRevisions + member _.countAll webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT COUNT(id) FROM page WHERE web_log_id = @webLogId" + addWebLogId cmd webLogId + return! count cmd } + member _.countListed webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- + """SELECT COUNT(id) + FROM page + WHERE web_log_id = @webLogId + AND show_in_page_list = @showInPageList""" + addWebLogId cmd webLogId + cmd.Parameters.AddWithValue ("@showInPageList", true) |> ignore + return! count cmd + } + + member _.findById pageId webLogId = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM page WHERE id = @id" + cmd.Parameters.AddWithValue ("@id", PageId.toString pageId) |> ignore + use! rdr = cmd.ExecuteReaderAsync () + match verifyWebLog webLogId (fun it -> it.webLogId) Map.toPage rdr with + | Some page -> + let! page = appendPageMeta page + return Some page + | None -> return None + } + + member this.findFullById pageId webLogId = backgroundTask { + match! this.findById pageId webLogId with + | Some page -> + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM page_permalink WHERE page_id = @pageId" + cmd.Parameters.AddWithValue ("@pageId", PageId.toString page.id) |> ignore + use! linkRdr = cmd.ExecuteReaderAsync () + let page = { page with priorPermalinks = toList Map.toPermalink linkRdr } + cmd.CommandText <- "SELECT * FROM page_revision WHERE page_id = @pageId" + use! revRdr = cmd.ExecuteReaderAsync () + return Some { page with revisions = toList Map.toRevision revRdr } + | None -> return None + } + member this.delete pageId webLogId = backgroundTask { match! this.findById pageId webLogId with | Some _ -> - let _ = Collection.Page.Delete (PageIdMapping.toBson pageId) - do! checkpoint () + use cmd = conn.CreateCommand () + cmd.CommandText <- "DELETE FROM page_revision WHERE page_id = @id" + cmd.Parameters.AddWithValue ("@id", PageId.toString pageId) |> ignore + do! write cmd + cmd.CommandText <- "DELETE FROM page_permalink WHERE page_id = @id" + do! write cmd + cmd.CommandText <- "DELETE FROM page_meta WHERE page_id = @id" + do! write cmd + cmd.CommandText <- "DELETE FROM page WHERE id = @id" + do! write cmd return true | None -> return false } member _.findByPermalink permalink webLogId = backgroundTask { - let! page = - Collection.Page.Find (fun p -> p.webLogId = webLogId && p.permalink = permalink) - |> tryFirst - return page |> Option.map pageWithoutRevisions + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT * FROM page WHERE web_log_id = @webLogId AND permalink = @link" + addWebLogId cmd webLogId + cmd.Parameters.AddWithValue ("@link", Permalink.toString permalink) |> ignore + use! rdr = cmd.ExecuteReaderAsync () + if rdr.Read () then + let! page = appendPageMeta (Map.toPage rdr) + return Some page + else + return None } member _.findCurrentPermalink permalinks webLogId = backgroundTask { - let! result = - Collection.Page.Find (fun p -> - p.webLogId = webLogId - && p.priorPermalinks |> List.exists (fun link -> permalinks |> List.contains link)) - |> tryFirst - return result |> Option.map (fun pg -> pg.permalink) + use cmd = conn.CreateCommand () + cmd.CommandText <- + """SELECT p.permalink + FROM page p + INNER JOIN page_permalink pp ON pp.page_id = p.id + WHERE p.web_log_id = @webLogId + AND pp.permalink IN (""" + permalinks + |> List.iteri (fun idx link -> + if idx > 0 then cmd.CommandText <- $"{cmd.CommandText}, " + cmd.CommandText <- $"{cmd.CommandText}@link{idx}" + cmd.Parameters.AddWithValue ($"@link{idx}", Permalink.toString link) |> ignore) + cmd.CommandText <- $"{cmd.CommandText})" + addWebLogId cmd webLogId + use! rdr = cmd.ExecuteReaderAsync () + return if rdr.Read () then Some (Map.toPermalink rdr) else None } member _.findFullByWebLog webLogId = @@ -265,7 +503,7 @@ type SQLiteData (conn : SqliteConnection) = member _.findFullById postId webLogId = Collection.Post.FindById (PostIdMapping.toBson postId) - |> verifyWebLog webLogId (fun p -> p.webLogId) + //|> verifyWebLog webLogId (fun p -> p.webLogId) member this.delete postId webLogId = backgroundTask { match! this.findFullById postId webLogId with @@ -357,7 +595,7 @@ type SQLiteData (conn : SqliteConnection) = member _.findById tagMapId webLogId = Collection.TagMap.FindById (TagMapIdMapping.toBson tagMapId) - |> verifyWebLog webLogId (fun tm -> tm.webLogId) + //|> verifyWebLog webLogId (fun tm -> tm.webLogId) member this.delete tagMapId webLogId = backgroundTask { match! this.findById tagMapId webLogId with @@ -516,7 +754,7 @@ type SQLiteData (conn : SqliteConnection) = member _.findById userId webLogId = Collection.WebLogUser.FindById (WebLogUserIdMapping.toBson userId) - |> verifyWebLog webLogId (fun u -> u.webLogId) + // |> verifyWebLog webLogId (fun u -> u.webLogId) member _.findByWebLog webLogId = Collection.WebLogUser.Find (fun wlu -> wlu.webLogId = webLogId) @@ -540,16 +778,223 @@ type SQLiteData (conn : SqliteConnection) = member _.startUp () = backgroundTask { - let _ = Collection.Category.EnsureIndex (fun c -> c.webLogId) - let _ = Collection.Comment.EnsureIndex (fun c -> c.postId) - let _ = Collection.Page.EnsureIndex (fun p -> p.webLogId) - let _ = Collection.Page.EnsureIndex (fun p -> p.authorId) - let _ = Collection.Post.EnsureIndex (fun p -> p.webLogId) - let _ = Collection.Post.EnsureIndex (fun p -> p.authorId) - let _ = Collection.TagMap.EnsureIndex (fun tm -> tm.webLogId) - let _ = Collection.WebLog.EnsureIndex (fun wl -> wl.urlBase) - let _ = Collection.WebLogUser.EnsureIndex (fun wlu -> wlu.webLogId) + let tableExists table = backgroundTask { + use cmd = conn.CreateCommand () + cmd.CommandText <- "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = @table" + cmd.Parameters.AddWithValue ("@table", table) |> ignore + let! count = cmd.ExecuteScalarAsync () + return (count :?> int) = 1 + } - do! checkpoint () + let! exists = tableExists "theme" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE theme ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE theme_template ( + theme_id TEXT NOT NULL REFERENCES theme (id), + name TEXT NOT NULL, + template TEXT NOT NULL, + PRIMARY KEY (theme_id, name))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE theme_asset ( + theme_id TEXT NOT NULL REFERENCES theme (id), + path TEXT NOT NULL, + updated_on TEXT NOT NULL, + data BINARY NOT NULL, + PRIMARY KEY (theme_id, path))""" + do! write cmd + + let! exists = tableExists "web_log" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE web_log ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + subtitle TEXT, + default_page TEXT NOT NULL, + theme_id TEXT NOT NULL REFERENCES theme (id), + url_base TEXT NOT NULL, + time_zone TEXT NOT NULL, + auto_htmx INTEGER NOT NULL DEFAULT 0)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE web_log_rss ( + web_log_id TEXT PRIMARY KEY REFERENCES web_log (id), + feed_enabled INTEGER NOT NULL DEFAULT 0, + feed_name TEXT NOT NULL, + items_in_feed INTEGER, + category_enabled INTEGER NOT NULL DEFAULT 0, + tag_enabled INTEGER NOT NULL DEFAULT 0, + copyright TEXT)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE web_log_feed ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + source TEXT NOT NULL, + path TEXT NOT NULL)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE web_log_feed_podcast ( + feed_id TEXT PRIMARY KEY REFERENCES web_log_feed (id), + title TEXT NOT NULL, + subtitle TEXT, + items_in_feed INTEGER NOT NULL, + summary TEXT NOT NULL, + displayed_author TEXT NOT NULL, + email TEXT NOT NULL, + image_url TEXT NOT NULL, + itunes_category TEXT NOT NULL, + itunes_subcategory TEXT, + explicit TEXT NOT NULL, + default_media_type TEXT, + media_base_url TEXT)""" + do! write cmd + + let! exists = tableExists "category" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE category ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + name TEXT NOT NULL, + description TEXT, + parent_id TEXT)""" + do! write cmd + + let! exists = tableExists "web_log_user" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE web_log_user ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + user_name TEXT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + preferred_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + url TEXT, + authorization_level TEXT NOT NULL)""" + do! write cmd + + let! exists = tableExists "page" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE page ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + author_id TEXT NOT NULL REFERENCES web_log_user (id), + title TEXT NOT NULL, + permalink TEXT NOT NULL, + published_on TEXT NOT NULL, + updated_on TEXT NOT NULL, + show_in_page_list INTEGER NOT NULL DEFAULT 0, + template TEXT, + page_text TEXT NOT NULL)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE page_meta ( + page_id TEXT NOT NULL REFERENCES page (id), + name TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (page_id, name, value))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE page_permalink ( + page_id TEXT NOT NULL REFERENCES page (id), + permalink TEXT NOT NULL, + PRIMARY KEY (page_id, permalink))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE page_revision ( + page_id TEXT NOT NULL REFERENCES page (id), + as_of TEXT NOT NULL, + revision_text TEXT NOT NULL, + PRIMARY KEY (page_id, as_of))""" + do! write cmd + + let! exists = tableExists "post" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE post ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + author_id TEXT NOT NULL REFERENCES web_log_user (id), + status TEXT NOT NULL, + title TEXT NOT NULL, + permalink TEXT NOT NULL, + published_on TEXT, + updated_on TEXT NOT NULL, + template TEXT, + post_text TEXT NOT NULL)""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_category ( + post_id TEXT NOT NULL REFERENCES post (id), + category_id TEXT NOT NULL REFERENCES category (id), + PRIMARY KEY (post_id, category_id))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_tag ( + post_id TEXT NOT NULL REFERENCES post (id), + tag TEXT NOT NULL, + PRIMARY KEY (post_id, tag))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_meta ( + post_id TEXT NOT NULL REFERENCES post (id), + name TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (post_id, name, value))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_permalink ( + post_id TEXT NOT NULL REFERENCES post (id), + permalink TEXT NOT NULL, + PRIMARY KEY (post_id, permalink))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_revision ( + post_id TEXT NOT NULL REFERENCES post (id), + as_of TEXT NOT NULL, + revision_text TEXT NOT NULL, + PRIMARY KEY (page_id, as_of))""" + do! write cmd + cmd.CommandText <- + """CREATE TABLE post_comment ( + id TEXT PRIMARY KEY, + post_id TEXT NOT NULL REFERENCES post(id), + in_reply_to_id TEXT, + name TEXT NOT NULL, + email TEXT NOT NULL, + url TEXT, + status TEXT NOT NULL, + posted_on TEXT NOT NULL, + comment_text TEXT NOT NULL)""" + do! write cmd + + let! exists = tableExists "tag_map" + if not exists then + use cmd = conn.CreateCommand () + cmd.CommandText <- + """CREATE TABLE tag_map ( + id TEXT PRIMARY KEY, + web_log_id TEXT NOT NULL REFERENCES web_log (id), + tag TEXT NOT NULL, + url_value TEXT NOT NULL)""" + do! write cmd }