diff --git a/changelog.md b/changelog.md index 1b0c222e1..1eec4fa39 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,9 @@ -Unreleased +Unreleased (3.1.1) ======== +- @parsonsmatt + - [#133](https://github.com/bitemyapp/esqueleto/pull/133): Added `renderQueryToText` and related functions. + 3.1.0 ======= diff --git a/src/Database/Esqueleto.hs b/src/Database/Esqueleto.hs index 52175c637..d082191ad 100644 --- a/src/Database/Esqueleto.hs +++ b/src/Database/Esqueleto.hs @@ -86,6 +86,12 @@ module Database.Esqueleto , insertSelectCount , (<#) , (<&>) + -- ** Rendering Queries + , renderQueryToText + , renderQuerySelect + , renderQueryUpdate + , renderQueryDelete + , renderQueryInsertInto -- * Internal.Language , From -- * RDBMS-specific modules diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 151af41eb..4a506969d 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -1994,8 +1994,8 @@ builderToText = TL.toStrict . TLB.toLazyTextWith defaultChunkSize -- -- Note: if you're curious about the SQL query being generated by -- @esqueleto@, instead of manually using this function (which is --- possible but tedious), you may just turn on query logging of --- @persistent@. +-- possible but tedious), see the 'renderQueryToText' function (along with +-- 'renderQuerySelect', 'renderQueryUpdate', etc). toRawSql :: (SqlSelect a r, BackendCompatible SqlBackend backend) => Mode -> (backend, IdentState) -> SqlQuery a -> (TLB.Builder, [PersistValue]) @@ -2031,6 +2031,90 @@ toRawSql mode (conn, firstIdentState) query = , makeLocking lockingClause ] +-- | Renders a 'SqlQuery' into a 'Text' value along with the list of +-- 'PersistValue's that would be supplied to the database for @?@ placeholders. +-- +-- You must ensure that the 'Mode' you pass to this function corresponds with +-- the actual 'SqlQuery'. If you pass a query that uses incompatible features +-- (like an @INSERT@ statement with a @SELECT@ mode) then you'll get a weird +-- result. +-- +-- @since 3.1.1 +renderQueryToText + :: (SqlSelect a r, BackendCompatible SqlBackend backend, Monad m) + => Mode + -- ^ Whether to render as an 'SELECT', 'DELETE', etc. + -> SqlQuery a + -- ^ The SQL query you want to render. + -> R.ReaderT backend m (T.Text, [PersistValue]) +renderQueryToText mode query = do + backend <- R.ask + let (builder, pvals) = toRawSql mode (backend, initialIdentState) query + pure (builderToText builder, pvals) + +-- | Renders a 'SqlQuery' into a 'Text' value along with the list of +-- 'PersistValue's that would be supplied to the database for @?@ placeholders. +-- +-- You must ensure that the 'Mode' you pass to this function corresponds with +-- the actual 'SqlQuery'. If you pass a query that uses incompatible features +-- (like an @INSERT@ statement with a @SELECT@ mode) then you'll get a weird +-- result. +-- +-- @since 3.1.1 +renderQuerySelect + :: (SqlSelect a r, BackendCompatible SqlBackend backend, Monad m) + => SqlQuery a + -- ^ The SQL query you want to render. + -> R.ReaderT backend m (T.Text, [PersistValue]) +renderQuerySelect = renderQueryToText SELECT + +-- | Renders a 'SqlQuery' into a 'Text' value along with the list of +-- 'PersistValue's that would be supplied to the database for @?@ placeholders. +-- +-- You must ensure that the 'Mode' you pass to this function corresponds with +-- the actual 'SqlQuery'. If you pass a query that uses incompatible features +-- (like an @INSERT@ statement with a @SELECT@ mode) then you'll get a weird +-- result. +-- +-- @since 3.1.1 +renderQueryDelete + :: (SqlSelect a r, BackendCompatible SqlBackend backend, Monad m) + => SqlQuery a + -- ^ The SQL query you want to render. + -> R.ReaderT backend m (T.Text, [PersistValue]) +renderQueryDelete = renderQueryToText DELETE + +-- | Renders a 'SqlQuery' into a 'Text' value along with the list of +-- 'PersistValue's that would be supplied to the database for @?@ placeholders. +-- +-- You must ensure that the 'Mode' you pass to this function corresponds with +-- the actual 'SqlQuery'. If you pass a query that uses incompatible features +-- (like an @INSERT@ statement with a @SELECT@ mode) then you'll get a weird +-- result. +-- +-- @since 3.1.1 +renderQueryUpdate + :: (SqlSelect a r, BackendCompatible SqlBackend backend, Monad m) + => SqlQuery a + -- ^ The SQL query you want to render. + -> R.ReaderT backend m (T.Text, [PersistValue]) +renderQueryUpdate = renderQueryToText UPDATE + +-- | Renders a 'SqlQuery' into a 'Text' value along with the list of +-- 'PersistValue's that would be supplied to the database for @?@ placeholders. +-- +-- You must ensure that the 'Mode' you pass to this function corresponds with +-- the actual 'SqlQuery'. If you pass a query that uses incompatible features +-- (like an @INSERT@ statement with a @SELECT@ mode) then you'll get a weird +-- result. +-- +-- @since 3.1.1 +renderQueryInsertInto + :: (SqlSelect a r, BackendCompatible SqlBackend backend, Monad m) + => SqlQuery a + -- ^ The SQL query you want to render. + -> R.ReaderT backend m (T.Text, [PersistValue]) +renderQueryInsertInto = renderQueryToText INSERT_INTO -- | (Internal) Mode of query being converted by 'toRawSql'. data Mode = diff --git a/src/Database/Esqueleto/Internal/Sql.hs b/src/Database/Esqueleto/Internal/Sql.hs index 6ab2be033..78186240c 100644 --- a/src/Database/Esqueleto/Internal/Sql.hs +++ b/src/Database/Esqueleto/Internal/Sql.hs @@ -61,6 +61,11 @@ module Database.Esqueleto.Internal.Sql , veryUnsafeCoerceSqlExprValue , veryUnsafeCoerceSqlExprValueList -- * Helper functions + , renderQueryToText + , renderQuerySelect + , renderQueryUpdate + , renderQueryDelete + , renderQueryInsertInto , makeOrderByNoNewline , uncommas' , parens diff --git a/test/Common/Test.hs b/test/Common/Test.hs index ce8805805..a5450fb40 100644 --- a/test/Common/Test.hs +++ b/test/Common/Test.hs @@ -65,10 +65,12 @@ import Database.Persist.TH import Test.Hspec import UnliftIO +import Database.Persist (PersistValue(..)) import Data.Conduit (ConduitT, (.|), runConduit) import qualified Data.Conduit.List as CL import qualified Data.List as L import qualified Data.Set as S +import qualified Data.Text as Text import qualified Data.Text.Lazy.Builder as TLB import qualified Data.Text.Internal.Lazy as TL import qualified Database.Esqueleto.Internal.Sql as EI @@ -1437,7 +1439,26 @@ testCountingRows run = do [Value n] <- select $ from $ return . countKind liftIO $ (n :: Int) `shouldBe` expected - +testRenderSql :: Run -> Spec +testRenderSql run = + describe "testRenderSql" $ do + it "works" $ do + (queryText, queryVals) <- run $ renderQuerySelect $ + from $ \p -> do + where_ $ p ^. PersonName ==. val "Johhny Depp" + pure (p ^. PersonName, p ^. PersonAge) + -- the different backends use different quote marks, so I filter them out + -- here instead of making a duplicate test + Text.filter (\c -> c `notElem` ['`', '"']) queryText + `shouldBe` + Text.unlines + [ "SELECT Person.name, Person.age" + , "FROM Person" + , "WHERE Person.name = ?" + ] + queryVals + `shouldBe` + [toPersistValue ("Johhny Depp" :: TL.Text)] @@ -1460,8 +1481,7 @@ tests run = do testMathFunctions run testCase run testCountingRows run - - + testRenderSql run insert' :: ( Functor m