refactor: remove SqlFragment and use SQL.Snippet

This commit is contained in:
steve-chavez
2023-06-22 21:46:16 -05:00
committed by Steve Chavez
parent 83cf15fb7e
commit 078c6ec08c
5 changed files with 115 additions and 124 deletions
+1 -1
View File
@@ -271,7 +271,7 @@ addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,re
Node <$> newReadPlan <*> (updateForest . hush $ Node <$> newReadPlan <*> pure forest) Node <$> newReadPlan <*> (updateForest . hush $ Node <$> newReadPlan <*> pure forest)
Nothing -> -- root case Nothing -> -- root case
let let
newFrom = QualifiedIdentifier mempty $ decodeUtf8 sourceCTEName newFrom = QualifiedIdentifier mempty sourceCTEName
newAlias = Just (qiName $ from rPlan) newAlias = Just (qiName $ from rPlan)
newReadPlan = case action of newReadPlan = case action of
-- the CTE for mutations/rpc is used as WITH sourceCTEName .. SELECT .. FROM sourceCTEName as alias, -- the CTE for mutations/rpc is used as WITH sourceCTEName .. SELECT .. FROM sourceCTEName as alias,
+4 -7
View File
@@ -24,8 +24,6 @@ import qualified Data.Text.Encoding as T
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet) import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
import qualified Hasql.DynamicStatements.Statement as SQL import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as SQL
import qualified Hasql.Transaction as SQL import qualified Hasql.Transaction as SQL
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
@@ -51,8 +49,8 @@ import PostgREST.Plan (CallReadPlan (..),
MutateReadPlan (..), MutateReadPlan (..),
WrappedReadPlan (..)) WrappedReadPlan (..))
import PostgREST.Plan.MutatePlan (MutatePlan (..)) import PostgREST.Plan.MutatePlan (MutatePlan (..))
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet, import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
pgFmtIdentList, intercalateSnippet,
setConfigLocal, setConfigLocal,
setConfigLocalJson) setConfigLocalJson)
import PostgREST.Query.Statements (ResultSet (..)) import PostgREST.Query.Statements (ResultSet (..))
@@ -254,7 +252,7 @@ setPgLocals AppConfig{..} claims role roleSettings req actualPgVersion = lift $
roleSettingsSql = setConfigLocal mempty <$> roleSettings roleSettingsSql = setConfigLocal mempty <$> roleSettings
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings) appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings)
searchPathSql = searchPathSql =
let schemas = pgFmtIdentList (iSchema req : configDbExtraSearchPath) in let schemas = escapeIdentList (iSchema req : configDbExtraSearchPath) in
setConfigLocal mempty ("search_path", schemas) setConfigLocal mempty ("search_path", schemas)
usesLegacyGucs = configDbUseLegacyGucs && actualPgVersion < pgVersion140 usesLegacyGucs = configDbUseLegacyGucs && actualPgVersion < pgVersion140
@@ -269,8 +267,7 @@ setPgLocals AppConfig{..} claims role roleSettings req actualPgVersion = lift $
runPreReq :: AppConfig -> DbHandler () runPreReq :: AppConfig -> DbHandler ()
runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequest conf) runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequest conf)
where where
stmt req = SQL.Statement stmt req = SQL.dynamicallyParameterized
("select " <> fromQi req <> "()") ("select " <> fromQi req <> "()")
HE.noParams
HD.noResult HD.noResult
(configDbPreparedStatements conf) (configDbPreparedStatements conf)
+44 -46
View File
@@ -66,45 +66,43 @@ getSelectsJoins rr@(Node ReadPlan{select, relName, relToParent=Just rel, relAggA
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
aggAlias = pgFmtIdent relAggAlias aggAlias = pgFmtIdent relAggAlias
correlatedSubquery sub al cond = correlatedSubquery sub al cond =
(if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> SQL.sql al <> " ON " <> cond (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
(sel, joi) = if relIsToOne rel (sel, joi) = if relIsToOne rel
then then
( if relIsSpread ( if relIsSpread
then SQL.sql aggAlias <> ".*" then aggAlias <> ".*"
else SQL.sql ("row_to_json(" <> aggAlias <> ".*) AS " <> aliasOrName) else "row_to_json(" <> aggAlias <> ".*) AS " <> aliasOrName
, correlatedSubquery subquery aggAlias "TRUE") , correlatedSubquery subquery aggAlias "TRUE")
else else
( SQL.sql $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> aliasOrName ( "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> aliasOrName
, correlatedSubquery ( , correlatedSubquery (
"SELECT json_agg(" <> SQL.sql aggAlias <> ") AS " <> SQL.sql aggAlias <> "SELECT json_agg(" <> aggAlias <> ") AS " <> aggAlias <>
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias "FROM (" <> subquery <> " ) AS " <> aggAlias
) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE") ) aggAlias $ if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE")
in in
(if null select && null forest then selects else sel:selects, joi:joins) (if null select && null forest then selects else sel:selects, joi:joins)
mutatePlanToQuery :: MutatePlan -> SQL.Snippet mutatePlanToQuery :: MutatePlan -> SQL.Snippet
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _ applyDefaults) = mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _ applyDefaults) =
"INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if null iCols then " " else "(" <> cols <> ") ") <> "INSERT INTO " <> fromQi mainQi <> (if null iCols then " " else "(" <> cols <> ") ") <>
fromJsonBodyF body iCols True False applyDefaults <> fromJsonBodyF body iCols True False applyDefaults <>
-- Only used for PUT -- Only used for PUT
(if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "pgrst_body") <$> putConditions)) <> (if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "pgrst_body") <$> putConditions)) <>
SQL.sql (BS.unwords [ maybe mempty (\(oncDo, oncCols) ->
maybe mempty (\(oncDo, oncCols) -> if null oncCols then
if null oncCols then mempty
mempty else
else " ON CONFLICT(" <> intercalateSnippet ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of
" ON CONFLICT(" <> BS.intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of IgnoreDuplicates ->
IgnoreDuplicates -> "DO NOTHING"
"DO NOTHING" MergeDuplicates ->
MergeDuplicates -> if null iCols
if null iCols then "DO NOTHING"
then "DO NOTHING" else "DO UPDATE SET " <> intercalateSnippet ", " ((pgFmtIdent . tfName) <> const " = EXCLUDED." <> (pgFmtIdent . tfName) <$> iCols)
else "DO UPDATE SET " <> BS.intercalate ", " ((pgFmtIdent . tfName) <> const " = EXCLUDED." <> (pgFmtIdent . tfName) <$> iCols) ) onConflct <> " " <>
) onConflct, returningF mainQi returnings
returningF mainQi returnings
])
where where
cols = BS.intercalate ", " $ pgFmtIdent . tfName <$> iCols cols = intercalateSnippet ", " $ pgFmtIdent . tfName <$> iCols
-- An update without a limit is always filtered with a WHERE -- An update without a limit is always filtered with a WHERE
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults) mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
@@ -112,54 +110,54 @@ mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings a
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select= -- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
-- the select has to be based on "returnings" to make computed overloaded functions not throw -- the select has to be based on "returnings" to make computed overloaded functions not throw
SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false" "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
| range == allRange = | range == allRange =
"UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <> "UPDATE " <> mainTbl <> " SET " <> nonRangeCols <> " " <>
fromJsonBodyF body uCols False False applyDefaults <> fromJsonBodyF body uCols False False applyDefaults <>
whereLogic <> " " <> whereLogic <> " " <>
SQL.sql (returningF mainQi returnings) returningF mainQi returnings
| otherwise = | otherwise =
"WITH " <> "WITH " <>
"pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <> "pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <>
"pgrst_affected_rows AS (" <> "pgrst_affected_rows AS (" <>
"SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <> "SELECT " <> rangeIdF <> " FROM " <> mainTbl <>
whereLogic <> " " <> whereLogic <> " " <>
orderF mainQi ordts <> " " <> orderF mainQi ordts <> " " <>
limitOffsetF range <> limitOffsetF range <>
") " <> ") " <>
"UPDATE " <> mainTbl <> " SET " <> SQL.sql rangeCols <> "UPDATE " <> mainTbl <> " SET " <> rangeCols <>
"FROM pgrst_affected_rows " <> "FROM pgrst_affected_rows " <>
"WHERE " <> SQL.sql whereRangeIdF <> " " <> "WHERE " <> whereRangeIdF <> " " <>
SQL.sql (returningF mainQi returnings) returningF mainQi returnings
where where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
mainTbl = SQL.sql (fromQi mainQi) mainTbl = fromQi mainQi
emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
nonRangeCols = BS.intercalate ", " (pgFmtIdent . tfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . tfName <$> uCols) nonRangeCols = intercalateSnippet ", " (pgFmtIdent . tfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . tfName <$> uCols)
rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent (tfName col) <> " = (SELECT " <> pgFmtIdent (tfName col) <> " FROM pgrst_update_body) ") <$> uCols) rangeCols = intercalateSnippet ", " ((\col -> pgFmtIdent (tfName col) <> " = (SELECT " <> pgFmtIdent (tfName col) <> " FROM pgrst_update_body) ") <$> uCols)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts) (whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings) mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
| range == allRange = | range == allRange =
"DELETE FROM " <> SQL.sql (fromQi mainQi) <> " " <> "DELETE FROM " <> fromQi mainQi <> " " <>
whereLogic <> " " <> whereLogic <> " " <>
SQL.sql (returningF mainQi returnings) returningF mainQi returnings
| otherwise = | otherwise =
"WITH " <> "WITH " <>
"pgrst_affected_rows AS (" <> "pgrst_affected_rows AS (" <>
"SELECT " <> SQL.sql rangeIdF <> " FROM " <> SQL.sql (fromQi mainQi) <> "SELECT " <> rangeIdF <> " FROM " <> fromQi mainQi <>
whereLogic <> " " <> whereLogic <> " " <>
orderF mainQi ordts <> " " <> orderF mainQi ordts <> " " <>
limitOffsetF range <> limitOffsetF range <>
") " <> ") " <>
"DELETE FROM " <> SQL.sql (fromQi mainQi) <> " " <> "DELETE FROM " <> fromQi mainQi <> " " <>
"USING pgrst_affected_rows " <> "USING pgrst_affected_rows " <>
"WHERE " <> SQL.sql whereRangeIdF <> " " <> "WHERE " <> whereRangeIdF <> " " <>
SQL.sql (returningF mainQi returnings) returningF mainQi returnings
where where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
@@ -177,17 +175,17 @@ callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar re
"LATERAL " <> callIt (fmtParams prms) "LATERAL " <> callIt (fmtParams prms)
callIt :: SQL.Snippet -> SQL.Snippet callIt :: SQL.Snippet -> SQL.Snippet
callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> SQL.sql (fromQi qi) <> "(" <> argument <> ")).*) pgrst_call" callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call"
| otherwise = SQL.sql (fromQi qi) <> "(" <> argument <> ") pgrst_call" | otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call"
fmtParams :: [RoutineParam] -> SQL.Snippet fmtParams :: [RoutineParam] -> SQL.Snippet
fmtParams prms = SQL.sql $ BS.intercalate ", " fmtParams prms = intercalateSnippet ", "
((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms) ((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms)
returnedColumns :: SQL.Snippet returnedColumns :: SQL.Snippet
returnedColumns returnedColumns
| null returnings = "*" | null returnings = "*"
| otherwise = SQL.sql $ BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnings) | otherwise = intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnings)
-- | SQL query meant for COUNTing the root node of the Tree. -- | SQL query meant for COUNTing the root node of the Tree.
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT. -- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
@@ -229,7 +227,7 @@ getQualifiedIdentifier rel mainQi tblAlias = case rel of
-- FROM clause plus implicit joins -- FROM clause plus implicit joins
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
fromF rel mainQi tblAlias = SQL.sql $ "FROM " <> fromF rel mainQi tblAlias = "FROM " <>
(case rel of (case rel of
Just ComputedRelationship{relFunction,relTable} -> fromQi relFunction <> "(" <> pgFmtIdent (qiName relTable) <> ")" Just ComputedRelationship{relFunction,relTable} -> fromQi relFunction <> "(" <> pgFmtIdent (qiName relTable) <> ")"
_ -> fromQi mainQi) <> _ -> fromQi mainQi) <>
+53 -53
View File
@@ -4,12 +4,9 @@
{-| {-|
Module : PostgREST.Query.SqlFragment Module : PostgREST.Query.SqlFragment
Description : Helper functions for PostgREST.QueryBuilder. Description : Helper functions for PostgREST.QueryBuilder.
Any function that outputs a SqlFragment should be in this module.
-} -}
module PostgREST.Query.SqlFragment module PostgREST.Query.SqlFragment
( noLocationF ( noLocationF
, SqlFragment
, asBinaryF , asBinaryF
, asCsvF , asCsvF
, asGeoJsonF , asGeoJsonF
@@ -24,7 +21,6 @@ module PostgREST.Query.SqlFragment
, orderF , orderF
, pgFmtColumn , pgFmtColumn
, pgFmtIdent , pgFmtIdent
, pgFmtIdentList
, pgFmtJoinCondition , pgFmtJoinCondition
, pgFmtLogicTree , pgFmtLogicTree
, pgFmtOrderTerm , pgFmtOrderTerm
@@ -34,12 +30,15 @@ module PostgREST.Query.SqlFragment
, responseStatusF , responseStatusF
, returningF , returningF
, singleParameter , singleParameter
, sourceCTE
, sourceCTEName , sourceCTEName
, unknownEncoder , unknownEncoder
, intercalateSnippet , intercalateSnippet
, explainF , explainF
, setConfigLocal , setConfigLocal
, setConfigLocalJson , setConfigLocalJson
, escapeIdent
, escapeIdentList
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -87,17 +86,16 @@ import PostgREST.SchemaCache.Routine (Routine (..),
import Protolude hiding (cast) import Protolude hiding (cast)
sourceCTEName :: Text
-- | A part of a SQL query that cannot be executed independently
type SqlFragment = ByteString
noLocationF :: SqlFragment
noLocationF = "array[]::text[]"
sourceCTEName :: SqlFragment
sourceCTEName = "pgrst_source" sourceCTEName = "pgrst_source"
simpleOperator :: SimpleOperator -> SqlFragment sourceCTE :: SQL.Snippet
sourceCTE = "pgrst_source"
noLocationF :: SQL.Snippet
noLocationF = "array[]::text[]"
simpleOperator :: SimpleOperator -> SQL.Snippet
simpleOperator = \case simpleOperator = \case
OpNotEqual -> "<>" OpNotEqual -> "<>"
OpContains -> "@>" OpContains -> "@>"
@@ -109,7 +107,7 @@ simpleOperator = \case
OpNotExtendsLeft -> "&>" OpNotExtendsLeft -> "&>"
OpAdjacent -> "-|-" OpAdjacent -> "-|-"
quantOperator :: QuantOperator -> SqlFragment quantOperator :: QuantOperator -> SQL.Snippet
quantOperator = \case quantOperator = \case
OpEqual -> "=" OpEqual -> "="
OpGreaterThanEqual -> ">=" OpGreaterThanEqual -> ">="
@@ -121,7 +119,7 @@ quantOperator = \case
OpMatch -> "~" OpMatch -> "~"
OpIMatch -> "~*" OpIMatch -> "~*"
ftsOperator :: FtsOperator -> SqlFragment ftsOperator :: FtsOperator -> SQL.Snippet
ftsOperator = \case ftsOperator = \case
FilterFts -> "@@ to_tsquery" FilterFts -> "@@ to_tsquery"
FilterFtsPlain -> "@@ plainto_tsquery" FilterFtsPlain -> "@@ plainto_tsquery"
@@ -149,8 +147,11 @@ pgBuildArrayLiteral vals =
"{" <> T.intercalate "," (escaped <$> vals) <> "}" "{" <> T.intercalate "," (escaped <$> vals) <> "}"
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833 -- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
pgFmtIdent :: Text -> SqlFragment pgFmtIdent :: Text -> SQL.Snippet
pgFmtIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\"" pgFmtIdent x = SQL.sql $ escapeIdent x
escapeIdent :: Text -> ByteString
escapeIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..` -- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
pgFmtLit :: Text -> Text pgFmtLit :: Text -> Text
@@ -168,12 +169,12 @@ trimNullChars = T.takeWhile (/= '\x0')
-- | -- |
-- Format a list of identifiers and separate them by commas. -- Format a list of identifiers and separate them by commas.
-- --
-- >>> pgFmtIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"] -- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\"" -- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
pgFmtIdentList :: [Text] -> SqlFragment escapeIdentList :: [Text] -> ByteString
pgFmtIdentList schemas = BS.intercalate ", " $ pgFmtIdent <$> schemas escapeIdentList schemas = BS.intercalate ", " $ escapeIdent <$> schemas
asCsvF :: SqlFragment asCsvF :: SQL.Snippet
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
where where
asCsvHeaderF = asCsvHeaderF =
@@ -181,20 +182,20 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
" FROM (" <> " FROM (" <>
" SELECT json_object_keys(r)::text as k" <> " SELECT json_object_keys(r)::text as k" <>
" FROM ( " <> " FROM ( " <>
" SELECT row_to_json(hh) as r from " <> sourceCTEName <> " as hh limit 1" <> " SELECT row_to_json(hh) as r from " <> sourceCTE <> " as hh limit 1" <>
" ) s" <> " ) s" <>
" ) a" <> " ) a" <>
")" ")"
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')" asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
asJsonSingleF :: Maybe Routine -> SqlFragment asJsonSingleF :: Maybe Routine -> SQL.Snippet
asJsonSingleF rout asJsonSingleF rout
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')" | returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
| otherwise = "coalesce(json_agg(_postgrest_t)->0, 'null')" | otherwise = "coalesce(json_agg(_postgrest_t)->0, 'null')"
where where
returnsScalar = maybe False funcReturnsScalar rout returnsScalar = maybe False funcReturnsScalar rout
asJsonF :: Maybe Routine -> SqlFragment asJsonF :: Maybe Routine -> SQL.Snippet
asJsonF rout asJsonF rout
| returnsSingleComposite = "coalesce(json_agg(_postgrest_t)->0, 'null')" | returnsSingleComposite = "coalesce(json_agg(_postgrest_t)->0, 'null')"
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')" | returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
@@ -205,16 +206,16 @@ asJsonF rout
Just r -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r) Just r -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r)
Nothing -> (False, False, False) Nothing -> (False, False, False)
asXmlF :: FieldName -> SqlFragment asXmlF :: FieldName -> SQL.Snippet
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')" asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
asGeoJsonF :: SqlFragment asGeoJsonF :: SQL.Snippet
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))" asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"
asBinaryF :: FieldName -> SqlFragment asBinaryF :: FieldName -> SQL.Snippet
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')" asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
locationF :: [Text] -> SqlFragment locationF :: [Text] -> SQL.Snippet
locationF pKeys = [qc|( locationF pKeys = [qc|(
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1) WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null')) SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
@@ -224,33 +225,32 @@ locationF pKeys = [qc|(
where where
fmtPKeys = T.intercalate "','" pKeys fmtPKeys = T.intercalate "','" pKeys
fromQi :: QualifiedIdentifier -> SqlFragment fromQi :: QualifiedIdentifier -> SQL.Snippet
fromQi t = (if T.null s then mempty else pgFmtIdent s <> ".") <> pgFmtIdent n fromQi t = (if T.null s then mempty else pgFmtIdent s <> ".") <> pgFmtIdent n
where where
n = qiName t n = qiName t
s = qiSchema t s = qiSchema t
pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment pgFmtColumn :: QualifiedIdentifier -> Text -> SQL.Snippet
pgFmtColumn table "*" = fromQi table <> ".*" pgFmtColumn table "*" = fromQi table <> ".*"
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
pgFmtField :: QualifiedIdentifier -> Field -> SQL.Snippet pgFmtField :: QualifiedIdentifier -> Field -> SQL.Snippet
pgFmtField table (c, []) = SQL.sql (pgFmtColumn table c) pgFmtField table (c, []) = pgFmtColumn table c
-- Using to_jsonb instead of to_json to avoid missing operator errors when filtering: -- Using to_jsonb instead of to_json to avoid missing operator errors when filtering:
-- "operator does not exist: json = unknown" -- "operator does not exist: json = unknown"
pgFmtField table (c, jp) = SQL.sql ("to_jsonb(" <> pgFmtColumn table c <> ")") <> pgFmtJsonPath jp pgFmtField table (c, jp) = "to_jsonb(" <> pgFmtColumn table c <> ")" <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> (Field, Maybe Cast, Maybe Alias) -> SQL.Snippet pgFmtSelectItem :: QualifiedIdentifier -> (Field, Maybe Cast, Maybe Alias) -> SQL.Snippet
pgFmtSelectItem table (f@(fName, jp), Nothing, alias) = pgFmtField table f <> SQL.sql (pgFmtAs fName jp alias) pgFmtSelectItem table (f@(fName, jp), Nothing, alias) = pgFmtField table f <> pgFmtAs fName jp alias
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc. -- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
-- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting. -- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting.
-- Not quoting should be fine, we validate the input on Parsers. -- Not quoting should be fine, we validate the input on Parsers.
pgFmtSelectItem table (f@(fName, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )" <> SQL.sql (pgFmtAs fName jp alias) pgFmtSelectItem table (f@(fName, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )" <> pgFmtAs fName jp alias
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body -- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
fromJsonBodyF :: Maybe LBS.ByteString -> [TypedField] -> Bool -> Bool -> Bool -> SQL.Snippet fromJsonBodyF :: Maybe LBS.ByteString -> [TypedField] -> Bool -> Bool -> Bool -> SQL.Snippet
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults = fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
SQL.sql
(if includeSelect then "SELECT " <> parsedCols <> " " else mempty) <> (if includeSelect then "SELECT " <> parsedCols <> " " else mempty) <>
"FROM (SELECT " <> jsonPlaceHolder <> " AS json_data) pgrst_payload, " <> "FROM (SELECT " <> jsonPlaceHolder <> " AS json_data) pgrst_payload, " <>
-- convert a json object into a json array, this way we can use json_to_recordset for all json payloads -- convert a json object into a json array, this way we can use json_to_recordset for all json payloads
@@ -266,12 +266,12 @@ fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
-- because it can't extract records with no columns (there's no valid syntax for the `AS (colName colType,...)` -- because it can't extract records with no columns (there's no valid syntax for the `AS (colName colType,...)`
-- part). But we still need to ensure as many rows are created as there are array elements. -- part). But we still need to ensure as many rows are created as there are array elements.
then SQL.sql $ jsonArrayElementsF <> "(" <> finalBodyF <> ") _ " then SQL.sql $ jsonArrayElementsF <> "(" <> finalBodyF <> ") _ "
else SQL.sql $ jsonToRecordsetF <> "(" <> finalBodyF <> ") AS _(" <> typedCols <> ") " <> if includeLimitOne then "LIMIT 1" else mempty else jsonToRecordsetF <> "(" <> SQL.sql finalBodyF <> ") AS _(" <> typedCols <> ") " <> if includeLimitOne then "LIMIT 1" else mempty
) <> ) <>
") pgrst_body " ") pgrst_body "
where where
parsedCols = BS.intercalate ", " $ fromQi . QualifiedIdentifier "pgrst_body" . tfName <$> fields parsedCols = intercalateSnippet ", " $ fromQi . QualifiedIdentifier "pgrst_body" . tfName <$> fields
typedCols = BS.intercalate ", " $ pgFmtIdent . tfName <> const " " <> encodeUtf8 . tfIRType <$> fields typedCols = intercalateSnippet ", " $ pgFmtIdent . tfName <> const " " <> SQL.sql . encodeUtf8 . tfIRType <$> fields
defsJsonb = SQL.sql $ BS.intercalate "," fieldsWDefaults defsJsonb = SQL.sql $ BS.intercalate "," fieldsWDefaults
fieldsWDefaults = mapMaybe (\case fieldsWDefaults = mapMaybe (\case
TypedField{tfName=nam, tfDefault=Just def} -> Just $ encodeUtf8 (pgFmtLit nam <> ", " <> def) TypedField{tfName=nam, tfDefault=Just def} -> Just $ encodeUtf8 (pgFmtLit nam <> ", " <> def)
@@ -302,12 +302,12 @@ pgFmtOrderTerm qi ot =
pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet
pgFmtFilter _ (FilterNullEmbed hasNot fld) = SQL.sql (pgFmtIdent fld) <> " IS " <> (if hasNot then "NOT" else mempty) <> " NULL" pgFmtFilter _ (FilterNullEmbed hasNot fld) = pgFmtIdent fld <> " IS " <> (if hasNot then "NOT" else mempty) <> " NULL"
pgFmtFilter _ (Filter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams pgFmtFilter _ (Filter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField table fld <> case oper of pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField table fld <> case oper of
Op op val -> " " <> SQL.sql (simpleOperator op) <> " " <> unknownLiteral val Op op val -> " " <> simpleOperator op <> " " <> unknownLiteral val
OpQuant op quant val -> " " <> SQL.sql (quantOperator op) <> " " <> case op of OpQuant op quant val -> " " <> quantOperator op <> " " <> case op of
OpLike -> fmtQuant quant $ unknownLiteral (T.map star val) OpLike -> fmtQuant quant $ unknownLiteral (T.map star val)
OpILike -> fmtQuant quant $ unknownLiteral (T.map star val) OpILike -> fmtQuant quant $ unknownLiteral (T.map star val)
_ -> fmtQuant quant $ unknownLiteral val _ -> fmtQuant quant $ unknownLiteral val
@@ -331,7 +331,7 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField
[""] -> "= ANY('{}') " [""] -> "= ANY('{}') "
_ -> "= ANY (" <> unknownLiteral (pgBuildArrayLiteral vals) <> ") " _ -> "= ANY (" <> unknownLiteral (pgBuildArrayLiteral vals) <> ") "
Fts op lang val -> " " <> SQL.sql (ftsOperator op) <> "(" <> ftsLang lang <> unknownLiteral val <> ") " Fts op lang val -> " " <> ftsOperator op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
where where
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ") ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
notOp = if hasNot then "NOT" else mempty notOp = if hasNot then "NOT" else mempty
@@ -343,7 +343,7 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) = pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
SQL.sql $ pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2 pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SQL.Snippet pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SQL.Snippet
pgFmtLogicTree qi (Expr hasNot op forest) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi <$> forest) <> ")" pgFmtLogicTree qi (Expr hasNot op forest) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi <$> forest) <> ")"
@@ -363,7 +363,7 @@ pgFmtJsonPath = \case
pgFmtJsonOperand (JKey k) = unknownLiteral k pgFmtJsonOperand (JKey k) = unknownLiteral k
pgFmtJsonOperand (JIdx i) = unknownLiteral i <> "::int" pgFmtJsonOperand (JIdx i) = unknownLiteral i <> "::int"
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SQL.Snippet
pgFmtAs _ [] Nothing = mempty pgFmtAs _ [] Nothing = mempty
pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
Just (JKey key) -> " AS " <> pgFmtIdent key Just (JKey key) -> " AS " <> pgFmtIdent key
@@ -375,7 +375,7 @@ pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
Nothing -> mempty Nothing -> mempty
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SqlFragment) countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SQL.Snippet)
countF countQuery shouldCount = countF countQuery shouldCount =
if shouldCount if shouldCount
then ( then (
@@ -385,11 +385,11 @@ countF countQuery shouldCount =
mempty mempty
, "null::bigint") , "null::bigint")
returningF :: QualifiedIdentifier -> [FieldName] -> SqlFragment returningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet
returningF qi returnings = returningF qi returnings =
if null returnings if null returnings
then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified
else "RETURNING " <> BS.intercalate ", " (pgFmtColumn qi <$> returnings) else "RETURNING " <> intercalateSnippet ", " (pgFmtColumn qi <$> returnings)
limitOffsetF :: NonnegRange -> SQL.Snippet limitOffsetF :: NonnegRange -> SQL.Snippet
limitOffsetF range = limitOffsetF range =
@@ -398,22 +398,22 @@ limitOffsetF range =
limit = maybe "ALL" (\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range limit = maybe "ALL" (\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range
offset = unknownEncoder (BS.pack . show $ rangeOffset range) offset = unknownEncoder (BS.pack . show $ rangeOffset range)
responseHeadersF :: SqlFragment responseHeadersF :: SQL.Snippet
responseHeadersF = currentSettingF "response.headers" responseHeadersF = currentSettingF "response.headers"
responseStatusF :: SqlFragment responseStatusF :: SQL.Snippet
responseStatusF = currentSettingF "response.status" responseStatusF = currentSettingF "response.status"
currentSettingF :: SqlFragment -> SqlFragment currentSettingF :: SQL.Snippet -> SQL.Snippet
currentSettingF setting = currentSettingF setting =
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15 -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
"nullif(current_setting('" <> setting <> "', true), '')" "nullif(current_setting('" <> setting <> "', true), '')"
mutRangeF :: QualifiedIdentifier -> [FieldName] -> (SqlFragment, SqlFragment) mutRangeF :: QualifiedIdentifier -> [FieldName] -> (SQL.Snippet, SQL.Snippet)
mutRangeF mainQi rangeId = mutRangeF mainQi rangeId =
( (
BS.intercalate " AND " $ (\col -> pgFmtColumn mainQi col <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_affected_rows") col) <$> rangeId intercalateSnippet " AND " $ (\col -> pgFmtColumn mainQi col <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_affected_rows") col) <$> rangeId
, BS.intercalate ", " (pgFmtColumn mainQi <$> rangeId) , intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
) )
orderF :: QualifiedIdentifier -> [OrderTerm] -> SQL.Snippet orderF :: QualifiedIdentifier -> [OrderTerm] -> SQL.Snippet
+13 -17
View File
@@ -61,25 +61,23 @@ prepareWrite selectQuery mutateQuery isInsert mt rep pKeys =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
snippet = snippet =
"WITH " <> SQL.sql sourceCTEName <> " AS (" <> mutateQuery <> ") " <> "WITH " <> sourceCTE <> " AS (" <> mutateQuery <> ") " <>
SQL.sql (
"SELECT " <> "SELECT " <>
"'' AS total_result_set, " <> "'' AS total_result_set, " <>
"pg_catalog.count(_postgrest_t) AS page_total, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <>
locF <> " AS header, " <> locF <> " AS header, " <>
bodyF <> " AS body, " <> bodyF <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status " responseStatusF <> " AS response_status " <>
) <>
"FROM (" <> selectF <> ") _postgrest_t" "FROM (" <> selectF <> ") _postgrest_t"
locF = locF =
if isInsert && rep == HeadersOnly if isInsert && rep == HeadersOnly
then BS.unwords [ then
"CASE WHEN pg_catalog.count(_postgrest_t) = 1", "CASE WHEN pg_catalog.count(_postgrest_t) = 1 " <>
"THEN coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")", "THEN coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ") " <>
"ELSE " <> noLocationF, "ELSE " <> noLocationF <> " " <>
"END"] "END"
else noLocationF else noLocationF
bodyF bodyF
@@ -91,7 +89,7 @@ prepareWrite selectQuery mutateQuery isInsert mt rep pKeys =
selectF selectF
-- prevent using any of the column names in ?select= when no response is returned from the CTE -- prevent using any of the column names in ?select= when no response is returned from the CTE
| rep /= Full = SQL.sql ("SELECT * FROM " <> sourceCTEName) | rep /= Full = "SELECT * FROM " <> sourceCTE
| otherwise = selectQuery | otherwise = selectQuery
decodeIt :: HD.Result ResultSet decodeIt :: HD.Result ResultSet
@@ -104,16 +102,15 @@ prepareRead selectQuery countQuery countTotal mt binaryField =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
snippet = snippet =
"WITH " <> "WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
SQL.sql sourceCTEName <> " AS ( " <> selectQuery <> " ) " <>
countCTEF <> " " <> countCTEF <> " " <>
SQL.sql ("SELECT " <> "SELECT " <>
countResultF <> " AS total_result_set, " <> countResultF <> " AS total_result_set, " <>
"pg_catalog.count(_postgrest_t) AS page_total, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <>
bodyF <> " AS body, " <> bodyF <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status " <> responseStatusF <> " AS response_status " <>
"FROM ( SELECT * FROM " <> sourceCTEName <> " ) _postgrest_t") "FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal
@@ -137,15 +134,14 @@ prepareCall rout callProcQuery selectQuery countQuery countTotal mt binaryField
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
snippet = snippet =
"WITH " <> SQL.sql sourceCTEName <> " AS (" <> callProcQuery <> ") " <> "WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
countCTEF <> countCTEF <>
SQL.sql (
"SELECT " <> "SELECT " <>
countResultF <> " AS total_result_set, " <> countResultF <> " AS total_result_set, " <>
"pg_catalog.count(_postgrest_t) AS page_total, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <>
bodyF <> " AS body, " <> bodyF <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status ") <> responseStatusF <> " AS response_status " <>
"FROM (" <> selectQuery <> ") _postgrest_t" "FROM (" <> selectQuery <> ") _postgrest_t"
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal