mirror of
https://github.com/circuithub/rel8.git
synced 2024-10-27 10:23:19 +03:00
Add a README documenting the differences
This commit is contained in:
parent
94896651ac
commit
f95eee5525
142
README.md
Normal file
142
README.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# Rel8
|
||||||
|
|
||||||
|
Welcome to Rel8! Rel8 is an API built on top of the
|
||||||
|
fantastic [Opaleye](https://hackage.haskell.org/package/opaleye) library to
|
||||||
|
provide an easy and type-safe way to interact with relational databases.
|
||||||
|
|
||||||
|
## Differences With Opaleye
|
||||||
|
|
||||||
|
### Table Definition
|
||||||
|
|
||||||
|
Opaleye doesn't really prescribe much in the way of table definition. Perhaps
|
||||||
|
the most idiomatic approach is to create a record where each field of the record
|
||||||
|
is parameterized. Then you provide type aliases that either use concrete Haskell
|
||||||
|
types or the `Column` type. The Opaleye tutorial demonstrates this as
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
data Birthday' a b = Birthday { bdName :: a, bdDay :: b }
|
||||||
|
type Birthday = Birthday' String Day
|
||||||
|
type BirthdayColumn = Birthday' (Column PGText) (Column PGDate)
|
||||||
|
```
|
||||||
|
|
||||||
|
In Rel8, the idiomatic approach is to create a record per table, but we use a
|
||||||
|
single type parameter to denote "where" this data is, and a type family to
|
||||||
|
interpret each column. The above example would be written as
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
data Birthday f = Birthday { bdName :: Anon f Text , bdName :: Anon f Day}
|
||||||
|
instance Table (Birthday Expr) (Birthday QueryResult)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Declaration
|
||||||
|
|
||||||
|
In Opaleye, the schema for a table is a value that you need to provide
|
||||||
|
explicitly. For the `Birthday'` type above, in Opaleye you would write
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
birthdayTable :: Table BirthdayColumn BirthdayColumn
|
||||||
|
birthdayTable = Table "birthdayTable"
|
||||||
|
(pBirthday Birthday { bdName = required "name"
|
||||||
|
, bdDay = required "birthday" })
|
||||||
|
```
|
||||||
|
|
||||||
|
In Rel8, the schema is directly specified in the type. If `birthday` is a table,
|
||||||
|
then our `Birthday` record would be written as:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
data Birthday f = Birthday
|
||||||
|
{ bdName :: Col f "name" 'NoDefault Text
|
||||||
|
, bdName :: Col f "birthday" 'NoDefault Day
|
||||||
|
} deriving (Generic)
|
||||||
|
|
||||||
|
instance Table (Birthday Expr) (Birthday QueryResult)
|
||||||
|
instance BaseTable Birthday where tableName = "birthdayTable"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Column Types
|
||||||
|
|
||||||
|
In Opaleye, column types form a distinct universe from ordinary Haskell types.
|
||||||
|
When we define tables, we use types such as `PGText` and `PGDate`.
|
||||||
|
|
||||||
|
In Rel8, you work entirely with the "result" types - the result of actually
|
||||||
|
querying data from the database. Haskell types map to exactly one type in the
|
||||||
|
database - `Text` is `text`, `Int64` is `bigint`, and so on. This mapping is
|
||||||
|
captured by the `DBType` type class.
|
||||||
|
|
||||||
|
### Aggregation
|
||||||
|
|
||||||
|
In Opaleye, aggregation is performed by using the `aggregate` function which
|
||||||
|
requires an `Aggregator`. Due to
|
||||||
|
the
|
||||||
|
[particularities of SQL](https://github.com/tomjaguarpaw/haskell-opaleye/issues/282),
|
||||||
|
`Aggregators` are not `Arrow`s, nor are they functions. This leaves us with
|
||||||
|
little option to build `Aggregator`s, though with `ProductProfunctor` (and some
|
||||||
|
template Haskell), the pain is somewhat eased. From the basic tutorial:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
aggregateWidgets :: Query (Widget (Column PGText) (Column PGText) (Column PGInt8)
|
||||||
|
(Column PGInt4) (Column PGFloat8))
|
||||||
|
aggregateWidgets = aggregate (pWidget (Widget { style = groupBy
|
||||||
|
, color = groupBy
|
||||||
|
, location = count
|
||||||
|
, quantity = sum
|
||||||
|
, radius = avg }))
|
||||||
|
(queryTable widgetTable)
|
||||||
|
```
|
||||||
|
|
||||||
|
This same approach is compatible with Rel8, but Rel8 has an alternative way to
|
||||||
|
perform aggregating. In Rel8, we have
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
aggregate :: AggregateTable exprsIn exprsOut => Query exprsIn -> Query exprsOut
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that we can `aggregate` any `Query`, provided the result of that
|
||||||
|
query is "valid" for aggregation. In Rel8, the above could be written as:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
aggregateWidgets :: Query (Widget Expr)
|
||||||
|
aggregateWidgets = aggregate $ proc _ -> do
|
||||||
|
widget <- queryTable -< ()
|
||||||
|
returnA -< Widget { style = groupBy
|
||||||
|
, color = groupBy
|
||||||
|
, location = count
|
||||||
|
, quantity = sum
|
||||||
|
, radius = avg
|
||||||
|
}
|
||||||
|
|
||||||
|
instance AggregateTable (Widget Aggregate) (Widget Expr)
|
||||||
|
```
|
||||||
|
|
||||||
|
While the two seem similar, I have found the latter to be a little easier to
|
||||||
|
work with, though the former is arguably closer to normal Haskell code
|
||||||
|
(`aggregate` being similar to `foldMap`). Personally, I'm not entirely happy
|
||||||
|
with either, so this space may change!
|
||||||
|
|
||||||
|
### `Column` vs `Expr`
|
||||||
|
|
||||||
|
The `Column` and `Expr` types are fundamentally same, but in order to avoid
|
||||||
|
orphan instances I currently need to provide a `Expr` `newtype`. While `Column`
|
||||||
|
should (morally) scope over `PG` types (`PGText`, `PGBool`, etc), `Expr` scopes
|
||||||
|
over Haskell types.
|
||||||
|
|
||||||
|
### Aggregation Functions
|
||||||
|
|
||||||
|
The built in aggregation functions in Rel8 are a little bit more honest to how
|
||||||
|
things work in PostgreSQL, at the expense of being less idiomatic Haskell. As
|
||||||
|
PostgreSQL has overloaded functions, the aggregations are also overloaded
|
||||||
|
functions provided by type classes. For example, we have
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
sum :: Expr Int16 -> Aggreagte Int64
|
||||||
|
sum :: Expr Int64 -> Aggregate Scientific
|
||||||
|
sum :: Expr Double -> Aggreagte Double
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outer Joins
|
||||||
|
|
||||||
|
Rel8 contains a row transforming type `MaybeTable` to capture the result of
|
||||||
|
outer joins. Opaleye deals with this by the use of `NullMaker`s. `MaybeTable`s,
|
||||||
|
when selected, will return `Maybe` of the actual row itself. You can project
|
||||||
|
columns out of a `MaybeTable` with the `$?` operator (function application on a
|
||||||
|
possibly-`null` row).
|
Loading…
Reference in New Issue
Block a user