Skip to content

Instantly share code, notes, and snippets.

@jxxcarlson
Created February 25, 2021 13:43
Show Gist options
  • Save jxxcarlson/d24bb7d31ddc0dc2973afb6172ebd263 to your computer and use it in GitHub Desktop.
Save jxxcarlson/d24bb7d31ddc0dc2973afb6172ebd263 to your computer and use it in GitHub Desktop.
{-
The idea behind module Db is to imitate part of the functionality of a
relation database which may have a number of indexes for fast searches.
In this example, the fundamental data structure is
type alias Db =
{ data : Dict Uuid Document
, authorDict : MultiBiDict Author Uuid
, titleDict : BiDict Title Uuid
, tagDict : MultiBiDict Tag Uuid
, pathDict : MultiBiDict String Uuid
, docNumberDict : BiDict Uuid Int
, keyWordAssociationList : List ( String, Uuid )
}
where the primary store is under the data field and the other fields
point to indexes. The dictionary that data points to has Uuid's
as keys and Documents as values. The authorDict points to a
MultiBiDict that relates Authors of Documents with Uuids of Documents.
An Author will typically have many documents, and it is important
to be able to retrieve all of them on O(log n) time rather than in O(n)
time. Similarly with the other indexes.
It would be a huge pain and hugely unreliable to manage the indexes
separately. The idea is for the primitive operations such as insert
and remove to do all of this work for the consumer of the module.
The code below was written early on in the course of the project and
really needs to be reviewed. For example, the function
setDocument : Document -> Db -> Db
updates both the data and titleDict fields, but should also update
other fields. In fact, I generally use
insert : Document -> Db -> Db
which does everything.
-}
module Db exposing
( Db
, authorDocumentNumbers
, empty
, get
, getAll
, getByAuthor
, getByDocumentNumber
, getByTag
, getByTags
, getByTitle
, getPublic
, insert
, remove
, setContent
, setDocument
, setTag
, setTags
, setTitle
, tag
)
import BiDict exposing (BiDict)
import Dict exposing (Dict)
import Document exposing (Document)
import Maybe.Extra
import MultiBiDict exposing (MultiBiDict)
import Search.Fuzzy as Fuzzy
import Set
type alias Uuid =
String
type alias Author =
String
type alias Tag =
String
type alias Title =
String
type alias Content =
String
type alias Db =
{ data : Dict Uuid Document
, authorDict : MultiBiDict Author Uuid
, titleDict : BiDict Title Uuid
, tagDict : MultiBiDict Tag Uuid
, pathDict : MultiBiDict String Uuid
, docNumberDict : BiDict Uuid Int
, keyWordAssociationList : List ( String, Uuid )
}
empty =
{ data = Dict.empty
, authorDict = MultiBiDict.empty
, titleDict = BiDict.empty
, tagDict = MultiBiDict.empty
, pathDict = MultiBiDict.empty
, docNumberDict = BiDict.empty
, keyWordAssociationList = []
}
{-|
> db = insert row1 empty
-}
insert : Document -> Db -> Db
insert document db =
{ db
| data = Dict.insert document.id document db.data
, authorDict = MultiBiDict.insert document.author document.id db.authorDict
, titleDict = BiDict.insert (normalize document.title) document.id db.titleDict
, pathDict = MultiBiDict.insert document.path document.id db.pathDict
, docNumberDict = BiDict.insert document.id document.documentNumber db.docNumberDict
, keyWordAssociationList = Fuzzy.insert (Document.keyWords document) document.id db.keyWordAssociationList
}
tag : Tag -> Uuid -> Db -> Db
tag tag_ id db =
{ db | tagDict = MultiBiDict.insert tag_ id db.tagDict }
-- GETTERS
get : Db -> Uuid -> Maybe Document
get db uuid =
Dict.get uuid db.data
getAll : Db -> List Document
getAll db =
db.data |> Dict.toList |> List.map Tuple.second
getByAuthor : Db -> Author -> List Document
getByAuthor db author =
MultiBiDict.get author db.authorDict
|> Set.toList
|> List.map (get db)
|> Maybe.Extra.values
numberOfDocumentsForAuthor : Db -> Author -> Int
numberOfDocumentsForAuthor db author =
getByAuthor db author |> List.length
authorDocumentNumbers : Db -> Dict Author Int
authorDocumentNumbers db =
db.authorDict
|> MultiBiDict.keys
|> List.map (\author -> (author, numberOfDocumentsForAuthor db author))
|> Dict.fromList
getByDocumentNumber : Db -> Int -> Maybe Document
getByDocumentNumber db docNumber =
let
maybeId =
BiDict.getReverse docNumber db.docNumberDict
|> Set.toList
|> List.head
in
case maybeId of
Nothing ->
Nothing
Just id ->
Dict.get id db.data
getPublic : Db -> List Document
getPublic db =
getAll db |> List.filter (\doc -> doc.public == True)
{-|
> getByTitle db "Intro to QM"
> Just { author = "jxxcarlson", content = "Yada yada", id = "123", tags = [], title = "Intro to QM" }
: Maybe (Row { content : String })
-}
getByTitle : Db -> Title -> Maybe Document
getByTitle db title =
BiDict.get (normalize title) db.titleDict
|> Maybe.andThen (get db)
getByTag : Tag -> Db -> List Document
getByTag tag_ db =
MultiBiDict.get (normalize tag_) db.tagDict
|> Set.toList
|> List.map (get db)
|> Maybe.Extra.values
{-|
> getByTags ["qm", "atom"] mdb
[{ author = "jxxcarlson", content = "Yada yada", id = "234", tags = ["qm","atom"], title = "The Atom" }]
-}
getByTags : List Tag -> Db -> List Document
getByTags tagList db =
case List.head tagList of
Nothing ->
[]
Just firstTag ->
let
remainingItems : List Document
remainingItems =
getByTag firstTag db
remainingTags : List Tag
remainingTags =
List.drop 1 tagList
filter_ : Tag -> List Document -> List Document
filter_ tag_ items_ =
List.filter (\item_ -> List.member tag_ item_.tags) items_
in
List.foldl filter_ remainingItems remainingTags
-- SETTERS
setTag : Db -> Uuid -> Tag -> Db
setTag db id_ tag_ =
{ db | tagDict = MultiBiDict.insert tag_ id_ db.tagDict }
setTagsInDocument : Uuid -> List Tag -> Db -> Db
setTagsInDocument id tags db =
case get db id of
Nothing ->
db
Just oldDocument ->
let
newDocument =
{ oldDocument | tags = tags }
in
{ db | data = Dict.insert id newDocument db.data }
setTags : Uuid -> List Tag -> Db -> Db
setTags id tags db =
List.foldl (\tag_ db_ -> setTag db_ id tag_) db tags
|> setTagsInDocument id tags
setTitle : Db -> Uuid -> String -> Db
setTitle db id title =
let
updater : Maybe Document -> Maybe Document
updater maybeRow =
case maybeRow of
Nothing ->
Nothing
Just row ->
Just { row | title = title }
in
{ db | data = Dict.update id updater db.data } |> resetTitleInIndex id title
resetTitleInIndex : Uuid -> String -> Db -> Db
resetTitleInIndex id title db =
case get db id of
Nothing ->
db
Just datum ->
{ db
| titleDict = BiDict.remove (normalize datum.title) db.titleDict |> BiDict.insert (normalize title) id
}
setContent : Db -> Uuid -> String -> Db
setContent db id content =
let
updater : Maybe Document -> Maybe Document
updater maybeDocument =
case maybeDocument of
Nothing ->
Nothing
Just document ->
Just { document | content = content }
in
{ db | data = Dict.update id updater db.data }
setDocument : Document -> Db -> Db
setDocument document db =
{ db | data = Dict.insert document.id document db.data
, titleDict = BiDict.insert (normalize document.title) document.id db.titleDict
}
-- DELETE
removeTitleFromIndex : BiDict Title Uuid -> Uuid -> BiDict Title Uuid
removeTitleFromIndex titleDict id =
let
maybeTitleToRemove =
BiDict.getReverse id titleDict |> Set.toList |> List.head
in
case maybeTitleToRemove of
Nothing ->
titleDict
Just title ->
BiDict.remove title titleDict
remove : Db -> Uuid -> Db
remove db id =
{ db
| data = Dict.remove id db.data
, titleDict = removeTitleFromIndex db.titleDict id
, docNumberDict = BiDict.remove id db.docNumberDict
}
-- |> List.map (Maybe.andThen (get db))
-- HELPERS
normalize : String -> String
normalize str =
str
|> String.toLower
|> String.replace " " "_"
-- TESTS
emptyDoc =
Document.empty
doc1 =
{ emptyDoc | id = "abc", title = "First" }
doc2 =
{ emptyDoc | id = "def", title = "Second" }
dbx =
empty |> insert doc1 |> insert doc2 |> setTags "abc" [ "Yada " ]
dby =
setTags "abc" [ "foo", "bar" ] dbx
dbz =
setTags "def" [ "foo", "bax" ] dby
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment