Make the Experimental module more prominent (#205)
* update README * add comments * update cabal * update changelog
This commit is contained in:
parent
f9a8088170
commit
583167adb0
96
README.md
96
README.md
@ -129,7 +129,50 @@ WHERE Person.age >= 18
|
||||
|
||||
Since `age` is an optional `Person` field, we use `just` to lift`val 18 :: SqlExpr (Value Int)` into `just (val 18) ::SqlExpr (Value (Maybe Int))`.
|
||||
|
||||
## Joins
|
||||
## Experimental/New Joins
|
||||
|
||||
There's a new way to write `JOIN`s in esqueleto! It has less potential for
|
||||
runtime errors and is much more powerful than the old syntax. To opt in to the
|
||||
new syntax, import:
|
||||
|
||||
```haskell
|
||||
import Database.Esqueleto.Experimental
|
||||
```
|
||||
|
||||
This will conflict with the definition of `from` and `on` in the
|
||||
`Database.Esqueleto` module, so you'll want to remove that import.
|
||||
|
||||
This style will become the new "default" in esqueleto-4.0.0.0, so it's a good
|
||||
idea to port your code to using it soon.
|
||||
|
||||
The module documentation in `Database.Esqueleto.Experimental` has many examples,
|
||||
and they won't be repeated here. Here's a quick sample:
|
||||
|
||||
```haskell
|
||||
select $ do
|
||||
(a :& b) <-
|
||||
from $
|
||||
Table @BlogPost
|
||||
`InnerJoin`
|
||||
Table @Person
|
||||
`on` do \(bp :& a) ->
|
||||
bp ^. BlogPostAuthorId ==. a ^. PersonId
|
||||
pure (a, b)
|
||||
```
|
||||
|
||||
Advantages:
|
||||
|
||||
- `ON` clause is attached directly to the relevant join, so you never need to
|
||||
worry about how they're ordered, nor will you ever run into bugs where the
|
||||
`on` clause is on the wrong `JOIN`
|
||||
- The `ON` clause lambda will all the available tables in it. This forbids
|
||||
runtime errors where an `ON` clause refers to a table that isn't in scope yet.
|
||||
- You can join on a table twice, and the aliases work out fine with the `ON`
|
||||
clause.
|
||||
- You can use `UNION`, `EXCEPT`, `INTERSECTION` etc with this new syntax!
|
||||
- You can reuse subqueries more easily.
|
||||
|
||||
## Legacy Joins
|
||||
|
||||
Implicit joins are represented by tuples.
|
||||
|
||||
@ -253,13 +296,13 @@ for that end we use `unsafeSqlFunction`. For example, if we wish to consult the
|
||||
|
||||
```haskell
|
||||
postgresTime :: (MonadIO m, MonadLogger m) => SqlWriteT m UTCTime
|
||||
postgresTime =
|
||||
postgresTime =
|
||||
result <- select (pure now)
|
||||
case result of
|
||||
[x] -> pure x
|
||||
_ -> error "now() is guaranteed to return a single result"
|
||||
where
|
||||
now :: SqlExpr (Value UTCTime)
|
||||
now :: SqlExpr (Value UTCTime)
|
||||
now = unsafeSqlFunction "now" ()
|
||||
```
|
||||
|
||||
@ -274,20 +317,20 @@ Do notice that `now` does not use any arguments, so we use `()` that is an insta
|
||||
`UnsafeSqlFunctionArgument` to represent no arguments, an empty list cast to a correct value
|
||||
will yield the same result as `()`.
|
||||
|
||||
We can also use `unsafeSqlFunction` for more complex functions with customs values using
|
||||
We can also use `unsafeSqlFunction` for more complex functions with customs values using
|
||||
`unsafeSqlValue` which turns any string into a sql value of whatever type we want, disclaimer:
|
||||
if you use it badly you will cause a runtime error. For example, say we want to try postgres'
|
||||
`date_part` function and get the day of a timestamp, we could use:
|
||||
|
||||
```haskell
|
||||
postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int
|
||||
postgresTimestampDay =
|
||||
postgresTimestampDay =
|
||||
result <- select (return $ dayPart date)
|
||||
case result of
|
||||
[x] -> pure x
|
||||
_ -> error "dayPart is guaranteed to return a single result"
|
||||
where
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
|
||||
date :: SqlExpr (Value UTCTime)
|
||||
date = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\'"
|
||||
@ -314,7 +357,7 @@ postgresTimestampDay = do
|
||||
[x] -> pure x
|
||||
_ -> error "dayPart is guaranteed to return a single result"
|
||||
where
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
|
||||
toTIMESTAMP :: SqlExpr (Value UTCTime) -> SqlExpr (Value UTCTime)
|
||||
toTIMESTAMP = unsafeSqlCastAs "TIMESTAMP"
|
||||
@ -333,7 +376,7 @@ on all queries, for example, if we have:
|
||||
|
||||
```haskell
|
||||
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
|
||||
myEvilQuery =
|
||||
myEvilQuery =
|
||||
select (return $ val ("hi\'; DROP TABLE foo; select \'bye\'" :: String)) >>= liftIO . print
|
||||
```
|
||||
|
||||
@ -349,10 +392,10 @@ Let's see an example of defining a new evil `now` function:
|
||||
|
||||
```haskell
|
||||
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
|
||||
myEvilQuery =
|
||||
myEvilQuery =
|
||||
select (return nowWithInjection) >>= liftIO . print
|
||||
where
|
||||
nowWithInjection :: SqlExpr (Value UTCTime)
|
||||
nowWithInjection :: SqlExpr (Value UTCTime)
|
||||
nowWithInjection = unsafeSqlFunction "0; DROP TABLE bar; select now" ([] :: [SqlExpr (Value Int)])
|
||||
```
|
||||
|
||||
@ -368,10 +411,10 @@ will be erased with no indication whatsoever. Another example of this behavior i
|
||||
|
||||
```haskell
|
||||
myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
|
||||
myEvilQuery =
|
||||
myEvilQuery =
|
||||
select (return $ dayPart dateWithInjection) >>= liftIO . print
|
||||
where
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int)
|
||||
dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
|
||||
dateWithInjection :: SqlExpr (Value UTCTime)
|
||||
dateWithInjection = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\');DROP TABLE bar; select (16"
|
||||
@ -387,11 +430,13 @@ This will print 16 and also erase the `bar` table. The main take away of this ex
|
||||
never use any user or third party input inside an unsafe function without first parsing it or
|
||||
heavily sanitizing the input.
|
||||
|
||||
### Tests and Postgres
|
||||
### Tests
|
||||
|
||||
To run the tests, do `stack test`. This tests all the backends, so you'll need
|
||||
to have MySQL and Postgresql installed.
|
||||
|
||||
#### Postgres
|
||||
|
||||
Using apt-get, you should be able to do:
|
||||
|
||||
```
|
||||
@ -417,23 +462,30 @@ withConn =
|
||||
|
||||
You can change these if you like but to just get them working set up as follows on linux:
|
||||
|
||||
```$ sudo -u postgres createuser esqutest```
|
||||
|
||||
```$ sudo -u postgres createdb esqutest```
|
||||
|
||||
```
|
||||
$ sudo -u postgres createuser esqutest
|
||||
$ sudo -u postgres createdb esqutest
|
||||
$ sudo -u postgres psql
|
||||
postgres=# \password esqutest
|
||||
```
|
||||
|
||||
|
||||
And on osx
|
||||
|
||||
```$ createuser esqutest```
|
||||
|
||||
```$ createdb esqutest```
|
||||
|
||||
```
|
||||
$ createuser esqutest
|
||||
$ createdb esqutest
|
||||
$ psql postgres
|
||||
postgres=# \password esqutest
|
||||
```
|
||||
|
||||
#### MySQL
|
||||
|
||||
To test MySQL, you'll need to have a MySQL server installation.
|
||||
Then, you'll need to create a database `esqutest` and a `'travis'@'localhost'`
|
||||
user which can access it:
|
||||
|
||||
```
|
||||
mysql> CREATE DATABASE esqutest;
|
||||
mysql> CREATE USER 'travis'@'localhost';
|
||||
mysql> GRANT ALL ON esqutest.* TO 'travis';
|
||||
```
|
||||
|
||||
28
changelog.md
28
changelog.md
@ -1,10 +1,32 @@
|
||||
3.3.4.0
|
||||
=======
|
||||
- @parsonsmatt
|
||||
- [#205](https://github.com/bitemyapp/esqueleto/pull/205)
|
||||
- More documentation on the `Experimental` module
|
||||
- `Database.Esqueleto.Experimental` now reexports `Database.Esqueleto`, so
|
||||
the new "approved" import syntax is less verbose. Before, you'd write:
|
||||
|
||||
```haskell
|
||||
import Database.Esqueleto hiding (from, on)
|
||||
import Database.Esqueleto.Experimental
|
||||
```
|
||||
|
||||
Now you can merely write:
|
||||
|
||||
```haskell
|
||||
import Database.Esqueleto.Experimental
|
||||
```
|
||||
|
||||
Users will get 'redundant import' warnings if they followed the original
|
||||
syntax, the solution is evident from the error message provided.
|
||||
|
||||
3.3.3.3
|
||||
=======
|
||||
- @belevy
|
||||
- [#191](https://github.com/bitemyapp/esqueleto/pull/191) - Bugfix rollup:
|
||||
- [#191](https://github.com/bitemyapp/esqueleto/pull/191) - Bugfix rollup:
|
||||
Fix issue with extra characters in generated SQL;
|
||||
Fix ToAliasReference for already referenced values;
|
||||
Fix Alias/Reference for Maybe Entity
|
||||
Fix ToAliasReference for already referenced values;
|
||||
Fix Alias/Reference for Maybe Entity
|
||||
- @maxgabriel
|
||||
- [#203](https://github.com/bitemyapp/esqueleto/pull/203) Document `isNothing`
|
||||
- @sestrella
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
name: esqueleto
|
||||
version: 3.3.3.3
|
||||
version: 3.3.4.0
|
||||
synopsis: Type-safe EDSL for SQL queries on persistent backends.
|
||||
description: @esqueleto@ is a bare bones, type-safe EDSL for SQL queries that works with unmodified @persistent@ SQL backends. Its language closely resembles SQL, so you don't have to learn new concepts, just new syntax, and it's fairly easy to predict the generated SQL and optimize it for your backend. Most kinds of errors committed when writing SQL are caught as compile-time errors---although it is possible to write type-checked @esqueleto@ queries that fail at runtime.
|
||||
.
|
||||
|
||||
@ -27,6 +27,11 @@
|
||||
--
|
||||
-- Other than identifier name clashes, @esqueleto@ does not
|
||||
-- conflict with @persistent@ in any way.
|
||||
--
|
||||
-- Note that the faciliites for @JOIN@ have been significantly improved in the
|
||||
-- "Database.Esqueleto.Experimental" module. The definition of 'from' and 'on'
|
||||
-- in this module will be replaced with those at the 4.0.0.0 version, so you are
|
||||
-- encouraged to migrate to the new method.
|
||||
module Database.Esqueleto
|
||||
( -- * Setup
|
||||
-- $setup
|
||||
|
||||
@ -12,6 +12,12 @@
|
||||
, PatternSynonyms
|
||||
#-}
|
||||
|
||||
-- | This module contains a new way (introduced in 3.3.3.0) of using @FROM@ in
|
||||
-- Haskell. The old method was a bit finicky and could permit runtime errors,
|
||||
-- and this new way is both significantly safer and much more powerful.
|
||||
--
|
||||
-- Esqueleto users are encouraged to migrate to this module, as it will become
|
||||
-- the default in a new major version @4.0.0.0@.
|
||||
module Database.Esqueleto.Experimental
|
||||
( -- * Setup
|
||||
-- $setup
|
||||
@ -39,9 +45,12 @@ module Database.Esqueleto.Experimental
|
||||
, ToAliasT
|
||||
, ToAliasReference(..)
|
||||
, ToAliasReferenceT
|
||||
-- * The Normal Stuff
|
||||
, module Database.Esqueleto
|
||||
)
|
||||
where
|
||||
|
||||
import Database.Esqueleto hiding (from, on, From(..))
|
||||
import qualified Control.Monad.Trans.Writer as W
|
||||
import qualified Control.Monad.Trans.State as S
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
|
||||
@ -137,6 +137,12 @@ where_ expr = Q $ W.tell mempty { sdWhereClause = Where expr }
|
||||
-- and tuple-joins do not need an 'on' clause, but 'InnerJoin' and the various
|
||||
-- outer joins do.
|
||||
--
|
||||
-- Note that this function will be replaced by the one in
|
||||
-- "Database.Esqueleto.Experimental" in version 4.0.0.0 of the library. The
|
||||
-- @Experimental@ module has a dramatically improved means for introducing
|
||||
-- tables and entities that provides more power and less potential for runtime
|
||||
-- errors.
|
||||
--
|
||||
-- If you don't include an 'on' clause (or include too many!) then a runtime
|
||||
-- exception will be thrown.
|
||||
--
|
||||
@ -1397,6 +1403,12 @@ class ToBaseId ent where
|
||||
|
||||
-- | @FROM@ clause: bring entities into scope.
|
||||
--
|
||||
-- Note that this function will be replaced by the one in
|
||||
-- "Database.Esqueleto.Experimental" in version 4.0.0.0 of the library. The
|
||||
-- @Experimental@ module has a dramatically improved means for introducing
|
||||
-- tables and entities that provides more power and less potential for runtime
|
||||
-- errors.
|
||||
--
|
||||
-- This function internally uses two type classes in order to
|
||||
-- provide some flexibility of how you may call it. Internally
|
||||
-- we refer to these type classes as the two different magics.
|
||||
@ -2180,7 +2192,7 @@ unsafeSqlBinOp op a b = unsafeSqlBinOp op (construct a) (construct b)
|
||||
-- a foreign (composite or not) key, so we enforce that it has
|
||||
-- no placeholders and split it on the commas.
|
||||
unsafeSqlBinOpComposite :: TLB.Builder -> TLB.Builder -> SqlExpr (Value a) -> SqlExpr (Value b) -> SqlExpr (Value c)
|
||||
unsafeSqlBinOpComposite op sep a b
|
||||
unsafeSqlBinOpComposite op sep a b
|
||||
| isCompositeKey a || isCompositeKey b = ERaw Parens $ compose (listify a) (listify b)
|
||||
| otherwise = unsafeSqlBinOp op a b
|
||||
where
|
||||
@ -2902,8 +2914,8 @@ aliasedEntityColumnIdent :: Ident -> FieldDef -> Ident
|
||||
aliasedEntityColumnIdent (I baseIdent) field =
|
||||
I (baseIdent <> "_" <> (unDBName $ fieldDB field))
|
||||
|
||||
aliasedColumnName :: Ident -> IdentInfo -> T.Text -> TLB.Builder
|
||||
aliasedColumnName (I baseIdent) info columnName =
|
||||
aliasedColumnName :: Ident -> IdentInfo -> T.Text -> TLB.Builder
|
||||
aliasedColumnName (I baseIdent) info columnName =
|
||||
useIdent info (I (baseIdent <> "_" <> columnName))
|
||||
|
||||
----------------------------------------------------------------------
|
||||
@ -2979,7 +2991,7 @@ instance PersistEntity a => SqlSelect (SqlExpr (Entity a)) (Entity a) where
|
||||
where
|
||||
process ed = uncommas $
|
||||
map ((name <>) . aliasName) $
|
||||
unescapedColumnNames ed
|
||||
unescapedColumnNames ed
|
||||
aliasName columnName = (fromDBName info columnName) <> " AS " <> aliasedColumnName aliasIdent info (unDBName columnName)
|
||||
name = useIdent info tableIdent <> "."
|
||||
ret = let ed = entityDef $ getEntityVal $ return expr
|
||||
@ -2988,7 +3000,7 @@ instance PersistEntity a => SqlSelect (SqlExpr (Entity a)) (Entity a) where
|
||||
where
|
||||
process ed = uncommas $
|
||||
map ((name <>) . aliasedColumnName baseIdent info . unDBName) $
|
||||
unescapedColumnNames ed
|
||||
unescapedColumnNames ed
|
||||
name = useIdent info sourceIdent <> "."
|
||||
ret = let ed = entityDef $ getEntityVal $ return expr
|
||||
in (process ed, mempty)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user