packages feed

data-basic-0.2.0.2: README.md

This is a guide on how to get started with basic. Each commit to this repository represents a
single step of the guide.
We'll cover the motivation and the general description of the library and how to use it,
which consists of declaring your schema, generating the model and manipulating the data.

### About
Basic is a database library with 4 main objectives, roughly prioritized from first to last.

#### Ease of use for the most common use cases
While SQL allows for a large number of ways to manipulate your data, people use a small subset of them in the majority of cases.

We aim to make these cases as painless as possible. It should never feel like you need to jump through
hoops to get a list of entities that satisfy a simple condition.

#### Type safety
The type level constraints should reflect your data constraints as best as possible. The library should never allow you to execute a query that doesn't make sense. Expressivity must never come before safety.

#### Flexibility
While we provide an escape hatch for writing raw queries to ensure you're never "stuck", the cases where it's needed should be few and far in between. If there's a way to provide an elegant and safe solution for a specific query, the library should allow it.

#### Ability to debug
If you mess up and/or something goes wrong we want to make it as easy as possible to fix it. Legible runtime errors are a must. Also, while Haskell libraries (especially the very "type safe" ones) are notorious for hard to understand compiler errors, we use custom type errors to try and cover some of the standard sources of mistakes. We try to provide useful descriptions of what went wrong and tips on how to fix it.

### Getting started
Prerequisites that are not covered by the tutorial:
* A PostgreSQL installation
* A database with a desired role configuration
* A new stack project (simple template) with a dependency on basic that builds

As a database-first library, your model will come from an SQL schema. If you have an existing database, you can use tools like pg_dump to get the schema out. If you're developing a new database, you will declare your tables and relations in an SQL file and then use it to initialize the database and generate the basic model.

Start off by creating a `./model/schema.sql` file. We'll be creating a rudimentary blog so we'll need an author and a post table.

```SQL
CREATE TABLE author (
    id int not null primary key,
    name text not null,
    registration_date timestamp not null
);

CREATE TABLE post (
    id int not null,
    name text not null,
    content text not null,
    creation_date timestamp not null,
    author int not null references author(id)
);

```

Load the schema into the database you created before. For example like this `psql -U postgres -d basic_guide -f model/schema.sql`

Now it's time to generate the model. Create a new module in your `src` directory. Call it `Model`.

```haskell
{-# LANGUAGE TemplateHaskell, DataKinds, FlexibleInstances, TypeFamilies, MultiParamTypeClasses #-}
{-# LANGUAGE DeriveGeneric, FlexibleContexts #-}
module Model where

import Data.Basic

mkFromFile "./model/schema.sql"
```

Add it to your cabal file.

This will generate all the declarations that you'll need to use the library. You will import this module from every module that requires access to the DB. To see the generated declarations, you can open the repl with `stack repl` and type `:browse Model`.

### Manipulating data
In your main module, import the `Model` and `Data.Basic` so we can start playing with data.
Let's start by inserting some data. All basic functions work in any monad with a `MonadEffect Basic` instance. Practically, this means it integrates well with a mtl-style codebase.

```haskell
sandbox :: MonadEffect1 Basic m => m ()
sandbox = return ()
```

For each table in your schema (say, author), basic will generate a new value called `newAuthor`. This represents a fresh table entry with no data. The underlying value is a record with one field for each column.

Before going futher, add dependencies to the `time`, `mtl`, `postgresql-simple` and `lens` libraries.

Basic uses the lens approach extensively and setting the fields is no exception. Here's how you do it

```haskell
{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}
module Main where

import Prelude hiding (id)
import Model
import Data.Basic (MonadEffect, Basic)
import Data.Time (getCurrentTime, getCurrentTimeZone, getCurrentTime, utcToLocalTime)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Function ((&))
import Control.Lens ((.~))

sandbox :: (MonadEffect Basic m, MonadIO m) => m ()
sandbox = do
    zone <- liftIO getCurrentTimeZone
    now <- liftIO getCurrentTime
    newAuthor
        & id .~ 0
        & name .~ "John"
        & registrationDate .~ utcToLocalTime zone now
        & print
        & liftIO

main :: IO ()
main = putStrLn "hello world"
```

Let's unpack this a bit. We first do a bit of `time` boilerplate to get the current `LocalTime`. Then we use the lens syntax to set fields. If you haven't seen it before, the pattern is `record & field .~ value`. This returns a new record so we can continue chaining. The `&` operator is actually the flipped version of `$` so we use it also to print the final value and lift that IO operation into our monad.

Now, there's no instance `MonadEffect Basic IO` because the regular old `IO` monad doesn't know anything about your database. To handle that constraint we use the `handleBasicPsql` function. It takes a database connection which we can get using the `connectPostgreSQL` function provided by `postgresql-simple`. So what we do is `handleBasicPsql conn sandbox` and it will provide the database functionality that `sandbox` needs, take care of that constraint leaving only the `MonadIO` of which the `IO` monad is an instance. Here's the final version of our `Main` module.

```haskell
{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}
module Main where

import Prelude hiding (id)
import Model
import Data.Basic (MonadEffect, Basic, handleBasicPsql)
import Data.Time (getCurrentTime, getCurrentTimeZone, getCurrentTime, utcToLocalTime)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Function ((&))
import Control.Lens ((.~))
import Database.PostgreSQL.Simple

sandbox :: (MonadEffect Basic m, MonadIO m) => m ()
sandbox = do
    zone <- liftIO getCurrentTimeZone
    now <- liftIO getCurrentTime
    newAuthor
        & id .~ 0
        & name .~ "John"
        & registrationDate .~ utcToLocalTime zone now
        & print
        & liftIO

main :: IO ()
main = do
    conn <- connectPostgreSQL "host=localhost port=5432 user=postgres dbname=basic_guide password=admin"
    handleBasicPsql conn sandbox
```
> make sure to modify the connection string

And guess what, we can actually run the thing now! Compile the project and run it, you should see something like:
```haskell
Entity {_getEntity = Author {_author_id = 0, _author_name = "John", _author_registration_date = 2017-06-09 23:11:16.0531469}}
```

You might complain that we didn't actually do anything with the database, and you're right, so let's actually insert this author in. How?

```haskell
    newAuthor
        & id .~ 0
        & name .~ "John"
        & registrationDate .~ utcToLocalTime zone now
        & insert
        & void
```
> add `insert` to the import list of `Data.Basic` and `import Control.Monad (void)`

Doesn't get much simpler than that.

Here's something cool. What if we forgot to set one of the required fields? Turns out, in basic, a partially filled in entity is a legitimate value. You can use the lens syntax to look at it's fields, you can even serialize it to JSON and back. The cool part is that you can't mess up by accessing an undefined field. Basic enforces this at the type level. Go ahead and try removing the `& name .~ "John"` line. When you try to compile it you should see something like

```
* Can't insert entity because the required field "name" is not set
```

### Querying data
Let's start by inserting a bunch of rows so we have something to work with.

```haskell
sandbox :: (MonadEffect Basic m, MonadIO m) => m ()
sandbox = do
    zone <- liftIO getCurrentTimeZone
    now <- liftIO getCurrentTime
    let localNow = utcToLocalTime zone now
    newAuthor
        & id .~ 0
        & name .~ "John"
        & registrationDate .~ localNow
        & insert
        & void

    newAuthor
        & id .~ 1
        & name .~ "Mark"
        & registrationDate .~ localNow
        & insert
        & void

    newAuthor
        & id .~ 2
        & name .~ "Steve"
        & registrationDate .~ localNow
        & insert
        & void

    newPost
        & id .~ 0
        & author .~ 0
        & creationDate .~ localNow
        & content .~ "ABC"
        & name .~ "ABC"
        & insert
        & void

    newPost
        & id .~ 1
        & author .~ 1
        & creationDate .~ localNow
        & content .~ "DEF"
        & name .~ "DEF"
        & insert
        & void

    newPost
        & id .~ 2
        & author .~ 2
        & creationDate .~ localNow
        & content .~ "GHI"
        & name .~ "GHI"
        & insert
        & void
```

While, to ensure type safety, the types of basic functions are a bit hard to parse, you can follow a simplified mental model. What you do is pretend that every table is just a list of entries. To manipulate the data you use the analog of standard list functions (mapping, filtering, folding...)

Let's get a list of all posts.

```haskell
posts <- allPosts -- Pretend `allPosts` has the type `[Post]`
```

How about all posts with an id less than 2?

```haskell
posts <- dfilter (\p -> (p ^. id) <. (2 :: Int)) allPosts
```
> `^.` is from lens, `dfilter` and `<.` are from `Data.Basic`

What about sorting them by name? And maybe getting only the first one.

```haskell
posts <- dtake 1 $ dsortOn (^. name) $ dfilter (\p -> (p ^. id) <. (2 :: Int)) allPosts
```

Some operations you can't do on lists, like deleting their content. Doing it in basic is easy. Filter the rows you want to delete and call `ddelete` on the whole thing.

```haskell
void $ ddelete $ dfilter (\p -> p ^. id ==. (1 :: Int)) allPosts
```

You can also update the values using the lens syntax like this

```haskell
void $ dupdate (\p -> p & id .~ (2 :: Int)) $ dfilter (\p -> p ^. id ==. (1 :: Int)) allPosts
```

Want to find the highest id of all posts?

```haskell
liftIO . print =<< dfoldMap (\p -> Max (p ^. id)) allPosts
```

Things get more interesting with joins and groups. To join two tables you use the `djoin` function at the start and then pretend your working with a list of all possible pairs. For grouping, you use the `dgroupOn` function to choose which field(s) to group on. Pretend the signature is something like `dgroupOn :: (a -> b) -> [a] -> [(b, [a])]`. You can then either map over that and fold the inner list (`dmap` and `dfoldMap`) or use the `dfoldMapInner` convenience functions with the pretend type signature `dfoldMapInner :: Monoid m => (a -> m) -> [(b, [a])] -> [(b, m)]`.

```haskell
res <-
    dfoldMapInner (\(p, a) -> Max (p ^. id)) $
    dgroupOn (\(p, a) -> a ^. id) $
    dfilter (\(p, a) -> p ^. author ==. a ^. id) $
    djoin allPosts allAuthors
```

Join all posts and authors, then filter the list so you're left with pairs (post, author of the post). Then group them by the author and, for each author, get it's highest post id.

Entities also know if they're fresh or they came from the database. This let's you conveniently update single rows like this

```haskell
[a] <- dfilter (\a -> a ^. id ==. (0 :: Int)) allAuthors
a & name .~ "New name"
  & save
  & void
```

What it does is it looks for the database entry with the same primary key and updates all the other fields. This works if the table actually _has_ a primary key.