Merge pull request #133 from bitemyapp/matt/render-query

Render queries as Text
This commit is contained in:
Chris Allen 2019-09-24 10:12:05 -05:00 committed by GitHub
commit b4bfe538f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 6 deletions

View File

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

View File

@ -86,6 +86,12 @@ module Database.Esqueleto
, insertSelectCount , insertSelectCount
, (<#) , (<#)
, (<&>) , (<&>)
-- ** Rendering Queries
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
-- * Internal.Language -- * Internal.Language
, From , From
-- * RDBMS-specific modules -- * RDBMS-specific modules

View File

@ -1995,8 +1995,8 @@ builderToText = TL.toStrict . TLB.toLazyTextWith defaultChunkSize
-- --
-- Note: if you're curious about the SQL query being generated by -- Note: if you're curious about the SQL query being generated by
-- @esqueleto@, instead of manually using this function (which is -- @esqueleto@, instead of manually using this function (which is
-- possible but tedious), you may just turn on query logging of -- possible but tedious), see the 'renderQueryToText' function (along with
-- @persistent@. -- 'renderQuerySelect', 'renderQueryUpdate', etc).
toRawSql toRawSql
:: (SqlSelect a r, BackendCompatible SqlBackend backend) :: (SqlSelect a r, BackendCompatible SqlBackend backend)
=> Mode -> (backend, IdentState) -> SqlQuery a -> (TLB.Builder, [PersistValue]) => Mode -> (backend, IdentState) -> SqlQuery a -> (TLB.Builder, [PersistValue])
@ -2032,6 +2032,90 @@ toRawSql mode (conn, firstIdentState) query =
, makeLocking lockingClause , 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'. -- | (Internal) Mode of query being converted by 'toRawSql'.
data Mode = data Mode =

View File

@ -61,6 +61,11 @@ module Database.Esqueleto.Internal.Sql
, veryUnsafeCoerceSqlExprValue , veryUnsafeCoerceSqlExprValue
, veryUnsafeCoerceSqlExprValueList , veryUnsafeCoerceSqlExprValueList
-- * Helper functions -- * Helper functions
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
, makeOrderByNoNewline , makeOrderByNoNewline
, uncommas' , uncommas'
, parens , parens

View File

@ -65,10 +65,12 @@ import Database.Persist.TH
import Test.Hspec import Test.Hspec
import UnliftIO import UnliftIO
import Database.Persist (PersistValue(..))
import Data.Conduit (ConduitT, (.|), runConduit) import Data.Conduit (ConduitT, (.|), runConduit)
import qualified Data.Conduit.List as CL import qualified Data.Conduit.List as CL
import qualified Data.List as L import qualified Data.List as L
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.Text as Text
import qualified Data.Text.Lazy.Builder as TLB import qualified Data.Text.Lazy.Builder as TLB
import qualified Data.Text.Internal.Lazy as TL import qualified Data.Text.Internal.Lazy as TL
import qualified Database.Esqueleto.Internal.Sql as EI import qualified Database.Esqueleto.Internal.Sql as EI
@ -1437,7 +1439,26 @@ testCountingRows run = do
[Value n] <- select $ from $ return . countKind [Value n] <- select $ from $ return . countKind
liftIO $ (n :: Int) `shouldBe` expected 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 testMathFunctions run
testCase run testCase run
testCountingRows run testCountingRows run
testRenderSql run
insert' :: ( Functor m insert' :: ( Functor m