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