Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the Experimental module more prominent #205

Merged
merged 4 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 74 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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" ()
```

Expand All @@ -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\'"
Expand All @@ -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"
Expand All @@ -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
```

Expand All @@ -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)])
```

Expand All @@ -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"
Expand All @@ -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:

```
Expand All @@ -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 changes: 25 additions & 3 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion esqueleto.cabal
Original file line number Diff line number Diff line change
@@ -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.
.
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Esqueleto.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Database/Esqueleto/Experimental.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 17 additions & 5 deletions src/Database/Esqueleto/Internal/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
--
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))

----------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down