-
Notifications
You must be signed in to change notification settings - Fork 107
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
Postgres Materialized CTE functions #354
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -2,7 +2,7 @@ cabal-version: 1.12 | |||||
|
||||||
name: esqueleto | ||||||
|
||||||
version: 3.5.11.2 | ||||||
version: 3.5.12.0 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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. | ||||||
. | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -38,7 +38,8 @@ import Database.Esqueleto.Internal.Internal hiding (From(..), from, on) | |||||||
-- PostgreSQL 12, non-recursive and side-effect-free queries may be inlined and | ||||||||
-- optimized accordingly if not declared @MATERIALIZED@ to get the previous | ||||||||
-- behaviour. See [the PostgreSQL CTE documentation](https://www.postgresql.org/docs/current/queries-with.html#id-1.5.6.12.7), | ||||||||
-- section Materialization, for more information. | ||||||||
-- section Materialization, for more information. To use a @MATERIALIZED@ query | ||||||||
-- in Esquelto, see functions 'withMaterialized' and 'withRecursiveMaterialized'. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
-- | ||||||||
-- /Since: 3.4.0.0/ | ||||||||
with :: ( ToAlias a | ||||||||
|
@@ -50,7 +51,7 @@ with query = do | |||||||
aliasedValue <- toAlias ret | ||||||||
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) | ||||||||
ident <- newIdentFor (DBName "cte") | ||||||||
let clause = CommonTableExpressionClause NormalCommonTableExpression ident (\info -> toRawSql SELECT info aliasedQuery) | ||||||||
let clause = CommonTableExpressionClause NormalCommonTableExpression (\_ _ -> "") ident (\info -> toRawSql SELECT info aliasedQuery) | ||||||||
Q $ W.tell mempty{sdCteClause = [clause]} | ||||||||
ref <- toAliasReference ident aliasedValue | ||||||||
pure $ From $ pure (ref, (\_ info -> (useIdent info ident, mempty))) | ||||||||
|
@@ -103,7 +104,8 @@ withRecursive baseCase unionKind recursiveCase = do | |||||||
ref <- toAliasReference ident aliasedValue | ||||||||
let refFrom = From (pure (ref, (\_ info -> (useIdent info ident, mempty)))) | ||||||||
let recursiveQuery = recursiveCase refFrom | ||||||||
let clause = CommonTableExpressionClause RecursiveCommonTableExpression ident | ||||||||
let noModifier _ _ = "" | ||||||||
let clause = CommonTableExpressionClause RecursiveCommonTableExpression noModifier ident | ||||||||
(\info -> (toRawSql SELECT info aliasedQuery) | ||||||||
<> ("\n" <> (unUnionKind unionKind) <> "\n", mempty) | ||||||||
<> (toRawSql SELECT info recursiveQuery) | ||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -34,6 +34,8 @@ module Database.Esqueleto.PostgreSQL | |||||
, forShareOf | ||||||
, filterWhere | ||||||
, values | ||||||
, withMaterialized | ||||||
, withNotMaterialized | ||||||
-- * Internal | ||||||
, unsafeSqlAggregateFunction | ||||||
) where | ||||||
|
@@ -46,15 +48,22 @@ import Control.Exception (throw) | |||||
import Control.Monad (void) | ||||||
import Control.Monad.IO.Class (MonadIO(..)) | ||||||
import qualified Control.Monad.Trans.Reader as R | ||||||
import qualified Control.Monad.Trans.Writer as W | ||||||
import Data.Int (Int64) | ||||||
import qualified Data.List.NonEmpty as NE | ||||||
import Data.Maybe | ||||||
import Data.Proxy (Proxy(..)) | ||||||
import qualified Data.Text.Internal.Builder as TLB | ||||||
import qualified Data.Text.Lazy as TL | ||||||
import qualified Data.Text.Lazy.Builder as TLB | ||||||
import Data.Time.Clock (UTCTime) | ||||||
import qualified Database.Esqueleto.Experimental as Ex | ||||||
import Database.Esqueleto.Internal.Internal hiding (random_) | ||||||
import qualified Database.Esqueleto.Experimental.From as Ex | ||||||
import Database.Esqueleto.Experimental.From.CommonTableExpression | ||||||
import Database.Esqueleto.Experimental.From.SqlSetOperation | ||||||
import Database.Esqueleto.Experimental.ToAlias | ||||||
import Database.Esqueleto.Experimental.ToAliasReference | ||||||
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on, random_) | ||||||
import Database.Esqueleto.Internal.PersistentImport hiding | ||||||
(uniqueFields, upsert, upsertBy) | ||||||
import Database.Persist.SqlBackend | ||||||
|
@@ -477,3 +486,79 @@ forUpdateOf lockableEntities onLockedBehavior = | |||||
forShareOf :: LockableEntity a => a -> OnLockedBehavior -> SqlQuery () | ||||||
forShareOf lockableEntities onLockedBehavior = | ||||||
putLocking $ PostgresLockingClauses [PostgresLockingKind PostgresForShare (Just $ LockingOfClause lockableEntities) onLockedBehavior] | ||||||
|
||||||
-- | @WITH@ @MATERIALIZED@ clause is used to introduce a | ||||||
-- [Common Table Expression (CTE)](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression) | ||||||
-- with the MATERIALIZED keyword. The MATERIALIZED keyword is only supported in PostgreSQL >= version 12. | ||||||
-- In Esqueleto, CTEs should be used as a subquery memoization tactic. PostgreSQL treats a materialized CTE as an optimization fence. | ||||||
-- A materialized CTE is always fully calculated, and is not "inlined" with other table joins. | ||||||
-- Without the MATERIALIZED keyword, PostgreSQL >= 12 may "inline" the CTE as though it was any other join. | ||||||
-- You should always verify that using a materialized CTE will in fact improve your performance | ||||||
-- over a regular subquery. | ||||||
-- | ||||||
-- @ | ||||||
-- select $ do | ||||||
-- cte <- withMaterialized subQuery | ||||||
-- cteResult <- from cte | ||||||
-- where_ $ cteResult ... | ||||||
-- pure cteResult | ||||||
-- @ | ||||||
-- | ||||||
-- | ||||||
-- For more information on materialized CTEs, see the PostgreSQL manual documentation on | ||||||
-- [Common Table Expression Materialization](https://www.postgresql.org/docs/14/queries-with.html#id-1.5.6.12.7). | ||||||
-- | ||||||
-- /Since: 3.5.12.0/ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
withMaterialized :: ( ToAlias a | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably want to also support non materialized so perhaps the materialization modifier can be passed in as an argument and we can provide helpers There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So here was what I was thinking with this PR:
Given that, when you say:
Are you referring to when no materialization modifier is specified, or when On the topic of supporting the I saw the following function pattern when investigating this feature:
To me, this implied I should add these functions:
But, for
So, I didn't add them; totally fine with adding them, I just want to make sure Also, I just re-examined the docs for pg 12-15, and I notice that recursive CTEs are So, given the above, I think the API should be one of these two choices:
Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @belevy I went ahead and made this change anyway; to me the alternate implementation seems more confusing. So for users of the library, if they want to use regular CTE, they use If they want the the postgresql specific behavior, they use But if you do want it to change, please let me know; i'd like to get this done sooner rather than later There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty happy with the API that is presented in this PR. |
||||||
, ToAliasReference a | ||||||
, SqlSelect a r | ||||||
) => SqlQuery a -> SqlQuery (Ex.From a) | ||||||
withMaterialized query = do | ||||||
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ query | ||||||
aliasedValue <- toAlias ret | ||||||
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) | ||||||
ident <- newIdentFor (DBName "cte") | ||||||
let clause = CommonTableExpressionClause NormalCommonTableExpression (\_ _ -> "MATERIALIZED ") ident (\info -> toRawSql SELECT info aliasedQuery) | ||||||
Q $ W.tell mempty{sdCteClause = [clause]} | ||||||
ref <- toAliasReference ident aliasedValue | ||||||
pure $ Ex.From $ pure (ref, (\_ info -> (useIdent info ident, mempty))) | ||||||
|
||||||
-- | @WITH@ @NOT@ @MATERIALIZED@ clause is used to introduce a | ||||||
-- [Common Table Expression (CTE)](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression) | ||||||
-- with the NOT MATERIALIZED keywords. These are only supported in PostgreSQL >= | ||||||
-- version 12. In Esqueleto, CTEs should be used as a subquery memoization | ||||||
-- tactic. PostgreSQL treats a materialized CTE as an optimization fence. A | ||||||
-- MATERIALIZED CTE is always fully calculated, and is not "inlined" with other | ||||||
-- table joins. Sometimes, this is undesirable, so postgres provides the NOT | ||||||
-- MATERIALIZED modifier to prevent this behavior, thus enabling it to possibly | ||||||
-- decide to treat the CTE as any other join. | ||||||
-- | ||||||
-- Given the above, it is unlikely that this function will be useful, as a | ||||||
-- normal join should be used instead, but is provided for completeness. | ||||||
-- | ||||||
-- @ | ||||||
-- select $ do | ||||||
-- cte <- withNotMaterialized subQuery | ||||||
-- cteResult <- from cte | ||||||
-- where_ $ cteResult ... | ||||||
-- pure cteResult | ||||||
-- @ | ||||||
-- | ||||||
-- | ||||||
-- For more information on materialized CTEs, see the PostgreSQL manual documentation on | ||||||
-- [Common Table Expression Materialization](https://www.postgresql.org/docs/14/queries-with.html#id-1.5.6.12.7). | ||||||
-- | ||||||
-- /Since: 3.5.12.0/ | ||||||
withNotMaterialized :: ( ToAlias a | ||||||
, ToAliasReference a | ||||||
, SqlSelect a r | ||||||
) => SqlQuery a -> SqlQuery (Ex.From a) | ||||||
withNotMaterialized query = do | ||||||
(ret, sideData) <- Q $ W.censor (\_ -> mempty) $ W.listen $ unQ query | ||||||
aliasedValue <- toAlias ret | ||||||
let aliasedQuery = Q $ W.WriterT $ pure (aliasedValue, sideData) | ||||||
ident <- newIdentFor (DBName "cte") | ||||||
let clause = CommonTableExpressionClause NormalCommonTableExpression (\_ _ -> "NOT MATERIALIZED ") ident (\info -> toRawSql SELECT info aliasedQuery) | ||||||
Q $ W.tell mempty{sdCteClause = [clause]} | ||||||
ref <- toAliasReference ident aliasedValue | ||||||
pure $ Ex.From $ pure (ref, (\_ info -> (useIdent info ident, mempty))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# docker-compose file for running postgres and mysql DBMS | ||
|
||
# If using this to run the tests, | ||
# while these containers are running (i.e. after something like) | ||
# (cd test; docker-compose up -d) | ||
# the tests must be told to use the hostname via MYSQL_HOST environment variable | ||
# e.g. something like: | ||
# MYSQL_HOST=127.0.0.1 stack test | ||
|
||
version: '3' | ||
services: | ||
postgres: | ||
image: 'postgres:15.2-alpine' | ||
environment: | ||
POSTGRES_USER: esqutest | ||
POSTGRES_PASSWORD: esqutest | ||
POSTGRES_DB: esqutest | ||
ports: | ||
- 5432:5432 | ||
mysql: | ||
image: 'mysql:8.0.32' | ||
environment: | ||
MYSQL_USER: travis | ||
MYSQL_PASSWORD: esqutest | ||
MYSQL_ROOT_PASSWORD: esqutest | ||
MYSQL_DATABASE: esqutest | ||
ports: | ||
- 3306:3306 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.