hlrdb: High-level Redis Database

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

A library for type-driven interaction with Redis


[Skip to Readme]

Properties

Versions 0.1.0.0, 0.2.0.0, 0.2.0.1, 0.3.0.0, 0.3.0.0, 0.3.1.0, 0.3.2.0, 0.4.0.0
Change log CHANGELOG.md
Dependencies base (>=4.9 && <5.0), base64-bytestring (>=1.0.0.1 && <1.1), bytestring (>=0.10.8.1 && <0.11), cryptonite (>=0.24 && <0.27), hashable, hedis, hlrdb-core (>=0.1.1 && <0.2), memory (>=0.14.8 && <0.15), random (>=1.1 && <1.2), store (>=0.5.1.1 && <0.6), time, unordered-containers [details]
License MIT
Author Identical Snowflake
Maintainer identicalsnowflake@protonmail.com
Category Database
Home page https://github.com/identicalsnowflake/hlrdb
Bug tracker https://github.com/identicalsnowflake/hlrdb/issues
Source repo head: git clone https://github.com/identicalsnowflake/hlrdb
Uploaded by identicalsnowflake at 2019-06-19T19:20:00Z

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for hlrdb-0.3.0.0

[back to package description]

High-level Redis Database

HLRDB is an opinionated, high-level, type-driven library for modeling Redis-backed database architecture.

This package provides an easy API for you to declare your data paths in Redis, but in doing so makes many decisions for you about how to serialize and deserialize values, construct identifiers, and define path names. If you want more control over these aspects, you may instead use the HLRDB Core package, which simply defines the commands and the abstract API without opining on these matters.

Overview

Redis is a hash table database with several builtin primitive data structures. It does not use SQL, but instead uses its own system of primitive commands. You may find primitive Haskell bindings for these commands in the Hedis library, on which this library depends. HLRDB provides a type-driven, high-level abstraction on top of this.

-- minimal end-to-end, runnable example
import Data.Store
import Database.Redis (checkedConnect,defaultConnectInfo,runRedis)
import HLRDB

newtype CommentId = CommentId Identifier deriving (Eq,Ord,Show,Store,IsIdentifier)
newtype Comment = Comment String deriving (Eq,Ord,Show,Store)

cidToComment :: RedisBasic CommentId (Maybe Comment)
cidToComment = declareBasic "canonical mapping from CommentId to Comment"

main :: IO ()
main = do
  -- connect to Redis
  rconn <- checkedConnect defaultConnectInfo

  cid :: CommentId <- genId

  c :: Maybe Comment <- runRedis rconn $ do
    -- create a comment
    set' cidToComment cid $ Comment "hi"
    -- read it back
    get cidToComment cid

  print c

Identifiers

Use newtypes for Identifier for your various data types:


newtype CommentId = CommentId Identifier deriving (Eq,Ord,Show,Store,IsIdentifier)

-- use genId to create new identifiers:
example :: IO CommentId
example = genId

Data structures

Redis structures are mostly indexed by two types: their identifier and their value. When you declare a structure, you need to provide a unique description, which serves two purposes: first, it helps document what the purpose of the path is, and second, the hash of this string is how HLRDB distinguishes between multiple paths of the same type.

Basic

-- RedisBasic is used when for standard key-value storage.
cidToComment :: RedisBasic CommentId (Maybe Comment)
cidToComment = declareBasic "canonical mapping from CommentId to Comment"

-- RedisIntegral will treat a non-existent value as 0
cidToScore :: RedisIntegral CommentId Integer
cidToScore = declareIntegral "comment score"

-- Use `declareBasicZero` to choose your own "zero" for the data type
threadIdToComments :: RedisBasic ThreadId (RoseTree CommentId)
threadIdToComments = declareBasicZero "reddit-style comment threads" Empty

Other Redis structures

For lists and sorted sets, you may optionally provide a TrimScheme (a record with two fields, softCardinality :: Integer and trimProbability :: Double). When provided, HLRDB will automatically trim the structures in Redis to their proper size whenever data is added.

-- hset, basically a sub-hash table with a few extra primitive commands
voteHSet :: RedisStructure (HSET CommentId) UserId Vote
voteHSet = declareHSet "whether a user has voted a comment up or down"

-- list, with automatic max-length management with TrimScheme
tidToComments :: RedisList ThreadId CommentId
tidToComments = declareList "non-recursive comment threads" $ Just $ TrimScheme 1000 0.1

-- sorted sets store items by golf score - lower is better. supports TrimScheme
popularItems :: RedisSSet UserId PostId
popularItems = declareSSet "popular content" $ Just $ TrimScheme 1000 0.01 -- 1k max; trim with probability 0.01

-- set is intuitive
blockedUsers :: RedisSet UserId UserId
blockedUsers = declareSet "a user's block list"

Global paths

You may use the global variants of the above to declare paths indexed simply on (), rather than an Identifier newtype:

bannedUsers :: RedisSet () UserId
bannedUsers = declareGlobalSet "global ban list"

Once you've declared any of the above structures, you may use the Redis monad to perform operations on them. You may find the operations available for each structure defined in the HLRDB/Structures folder (found in hlrdb-core) for that particular structure. The commands are similar to the original Redis API, but have been cleaned up and re-imagined to support more of a Haskell dialect (e.g., list commands do not crash when passed [] as they do in Redis).

Lookup Aggregation

You may lift RedisBasic i v (and RedisIntegral i v, which is a subtype) paths to i ⟿ v queries, which can be combined together in several ways, resulting in a single mget command being executed in Redis. This allows constructing detailed data views in an efficient manner.

If you prefer, Query i v is a non-infix alias for i ⟿ v. You may also use the ASCII version, ~~>.


newtype Views = Views Integer deriving (Show,Eq,Ord,Num,Enum,Real,Integral)
newtype Likes = Likes Integer deriving (Show,Eq,Ord,Num,Enum,Real,Integral)


cidToViews :: RedisIntegral CommentId Views
cidToViews = declareIntegral "comment views"

cidToLikes :: RedisIntegral CommentId Likes
cidToLikes = declareIntegral "comment likes"


queryBoth :: CommentId ⟿ (Views , Likes)
queryBoth = (,) <$> liftq cidToViews <*> liftq cidToLikes

reifyToRedis :: CommentId -> Redis (Views , Likes)
reifyToRedis = mget queryBoth

I've written an in-depth article discussing aggregation here, but the two most important takeaways are that is a Traversing Profunctor and an Applicative.

Demo

There is a simple demo repository demonstrating the end-to-end process of defining data models and performing read/write actions.