Make the Experimental module more prominent (#205)

* update README

* add comments

* update cabal

* update changelog
This commit is contained in:
Matt Parsons 2020-09-17 14:52:38 -06:00 committed by GitHub
parent f9a8088170
commit 583167adb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 131 additions and 31 deletions

View File

@ -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';
```

View File

@ -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

View File

@ -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.
.

View File

@ -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

View File

@ -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)

View File

@ -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)