diff --git a/README.md b/README.md index e104c11..9906a99 100644 --- a/README.md +++ b/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'; +``` diff --git a/changelog.md b/changelog.md index 1974f0c..6afbc96 100644 --- a/changelog.md +++ b/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 diff --git a/esqueleto.cabal b/esqueleto.cabal index 41d3d5a..1fd0f78 100644 --- a/esqueleto.cabal +++ b/esqueleto.cabal @@ -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. . diff --git a/src/Database/Esqueleto.hs b/src/Database/Esqueleto.hs index 3268202..eda4272 100644 --- a/src/Database/Esqueleto.hs +++ b/src/Database/Esqueleto.hs @@ -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 diff --git a/src/Database/Esqueleto/Experimental.hs b/src/Database/Esqueleto/Experimental.hs index 342dd12..b7fe293 100644 --- a/src/Database/Esqueleto/Experimental.hs +++ b/src/Database/Esqueleto/Experimental.hs @@ -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) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 3c27d4a..0920806 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -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)