refactor: add PreQuery module

Small step towards solving #3934.

Adds a module that builds SQL snippets for the transaction variables
and the pre-request function.
This commit is contained in:
steve-chavez
2025-07-23 10:06:32 +02:00
committed by Steve Chavez
parent 5e0da40ac4
commit aa58e37f03
3 changed files with 75 additions and 36 deletions
+1
View File
@@ -71,6 +71,7 @@ library
PostgREST.Network PostgREST.Network
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.PreQuery
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
PostgREST.Query.SqlFragment PostgREST.Query.SqlFragment
PostgREST.Query.Statements PostgREST.Query.Statements
+9 -36
View File
@@ -11,7 +11,6 @@ module PostgREST.Query
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S import qualified Data.Set as S
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
@@ -22,6 +21,7 @@ import qualified Hasql.Transaction as SQL
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Query.PreQuery as PreQuery
import qualified PostgREST.Query.QueryBuilder as QueryBuilder import qualified PostgREST.Query.QueryBuilder as QueryBuilder
import qualified PostgREST.Query.Statements as Statements import qualified PostgREST.Query.Statements as Statements
import qualified PostgREST.SchemaCache as SchemaCache import qualified PostgREST.SchemaCache as SchemaCache
@@ -32,7 +32,6 @@ import PostgREST.ApiRequest (ApiRequest (..),
import PostgREST.ApiRequest.Preferences (PreferCount (..), import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferHandling (..), PreferHandling (..),
PreferMaxAffected (..), PreferMaxAffected (..),
PreferTimezone (..),
PreferTransaction (..), PreferTransaction (..),
Preferences (..), Preferences (..),
shouldCount) shouldCount)
@@ -48,11 +47,6 @@ import PostgREST.Plan (ActionPlan (..),
InfoPlan (..), InfoPlan (..),
InspectPlan (..)) InspectPlan (..))
import PostgREST.Plan.MutatePlan (MutatePlan (..)) import PostgREST.Plan.MutatePlan (MutatePlan (..))
import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
intercalateSnippet,
setConfigWithConstantName,
setConfigWithConstantNameJSON,
setConfigWithDynamicName)
import PostgREST.Query.Statements (ResultSet (..)) import PostgREST.Query.Statements (ResultSet (..))
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
@@ -90,8 +84,8 @@ query config AuthResult{..} apiReq (Db plan) sCache =
txMode = planTxMode plan txMode = planTxMode plan
(mainActionQuery, mainSQLQuery) = actionQuery plan config apiReq sCache (mainActionQuery, mainSQLQuery) = actionQuery plan config apiReq sCache
dbHandler = do dbHandler = do
setPgLocals plan config authClaims authRole apiReq runTxVarQuery plan config authClaims authRole apiReq
runPreReq config runPreReqQuery config
mainActionQuery mainActionQuery
planTxMode :: DbActionPlan -> SQL.Mode planTxMode :: DbActionPlan -> SQL.Mode
@@ -263,38 +257,17 @@ optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
shouldRollback = shouldRollback =
preferTransaction == Just Rollback preferTransaction == Just Rollback
-- | Set transaction scoped settings runTxVarQuery :: DbActionPlan -> AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> ApiRequest -> DbHandler ()
setPgLocals :: DbActionPlan -> AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> ApiRequest -> DbHandler () runTxVarQuery dbActPlan conf@AppConfig{..} claims role apireq = lift $
setPgLocals dbActPlan AppConfig{..} claims role ApiRequest{..} = lift $
SQL.statement mempty $ SQL.dynamicallyParameterized SQL.statement mempty $ SQL.dynamicallyParameterized
-- To ensure `GRANT SET ON PARAMETER <superuser_setting> TO authenticator` works, the role settings must be set before the impersonated role. (PreQuery.txVarQuery dbActPlan conf claims role apireq)
-- Otherwise the GRANT SET would have to be applied to the impersonated role. See https://github.com/PostgREST/postgrest/issues/3045
("select " <> intercalateSnippet ", " (searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ funcSettingsSql ++ appSettingsSql))
HD.noResult configDbPreparedStatements HD.noResult configDbPreparedStatements
where
methodSql = setConfigWithConstantName ("request.method", iMethod)
pathSql = setConfigWithConstantName ("request.path", iPath)
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
roleSql = [setConfigWithConstantName ("role", role)]
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup role configRoleSettings)
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
timezoneSql = maybe mempty (\(PreferTimezone tz) -> [setConfigWithConstantName ("timezone", tz)]) $ preferTimezone iPreferences
funcSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> funcSettings
searchPathSql =
let schemas = escapeIdentList (iSchema : configDbExtraSearchPath) in
setConfigWithConstantName ("search_path", schemas)
funcSettings = case dbActPlan of
DbCall CallReadPlan{crProc} -> pdFuncSettings crProc
_ -> mempty
-- | Runs the pre-request function. runPreReqQuery :: AppConfig -> DbHandler ()
runPreReq :: AppConfig -> DbHandler () runPreReqQuery conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequest conf)
runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequest conf)
where where
stmt req = SQL.dynamicallyParameterized stmt req = SQL.dynamicallyParameterized
("select " <> fromQi req <> "()") (PreQuery.preReqQuery req)
HD.noResult HD.noResult
(configDbPreparedStatements conf) (configDbPreparedStatements conf)
+65
View File
@@ -0,0 +1,65 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module : PostgREST.Query.PreQuery
Description : Builds queries that run prior to the main query
-}
module PostgREST.Query.PreQuery
( txVarQuery
, preReqQuery
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.HashMap.Strict as HM
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (PreferTimezone (..),
Preferences (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.Plan (CallReadPlan (..),
DbActionPlan (..))
import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
intercalateSnippet,
setConfigWithConstantName,
setConfigWithConstantNameJSON,
setConfigWithDynamicName)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Routine (Routine (..))
import Protolude hiding (Handler)
-- sets transaction variables
txVarQuery :: DbActionPlan -> AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> ApiRequest -> SQL.Snippet
txVarQuery dbActPlan AppConfig{..} claims role ApiRequest{..} =
-- To ensure `GRANT SET ON PARAMETER <superuser_setting> TO authenticator` works, the role settings must be set before the impersonated role.
-- Otherwise the GRANT SET would have to be applied to the impersonated role. See https://github.com/PostgREST/postgrest/issues/3045
"select " <> intercalateSnippet ", " (
searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ funcSettingsSql ++ appSettingsSql
)
where
methodSql = setConfigWithConstantName ("request.method", iMethod)
pathSql = setConfigWithConstantName ("request.path", iPath)
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
roleSql = [setConfigWithConstantName ("role", role)]
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup role configRoleSettings)
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
timezoneSql = maybe mempty (\(PreferTimezone tz) -> [setConfigWithConstantName ("timezone", tz)]) $ preferTimezone iPreferences
funcSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> funcSettings
searchPathSql =
let schemas = escapeIdentList (iSchema : configDbExtraSearchPath) in
setConfigWithConstantName ("search_path", schemas)
funcSettings = case dbActPlan of
DbCall CallReadPlan{crProc} -> pdFuncSettings crProc
_ -> mempty
-- runs the pre-request function
preReqQuery :: QualifiedIdentifier -> SQL.Snippet
preReqQuery preRequest = "select " <> fromQi preRequest <> "()"