perf: enable prepared statements for GET

- Parametrize filters
- Parametrize LIMIT/OFFSET
- Parametrize JSON path(select=col->$1)
- Single parameter for IN(use ANY)
- Also enable prepared statement for the EXPLAIN
  used on the estimated count.
This commit is contained in:
steve-chavez
2020-11-23 19:00:50 -05:00
committed by Steve Chavez
parent 8e58b56d5c
commit 4bd5e6bd82
5 changed files with 135 additions and 115 deletions
+65 -32
View File
@@ -18,6 +18,10 @@ import qualified Data.Text as T (intercalate,
takeWhile, takeWhile,
toLower) toLower)
import qualified Hasql.DynamicStatements.Snippet as H import qualified Hasql.DynamicStatements.Snippet as H
import PostgREST.RangeQuery (NonnegRange,
allRange,
rangeLimit,
rangeOffset)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (cast, import Protolude hiding (cast,
intercalate, intercalate,
@@ -28,6 +32,8 @@ import Text.InterpolatedString.Perl6 (qc)
import qualified Hasql.Encoders as HE import qualified Hasql.Encoders as HE
import Data.Foldable (foldr1)
noLocationF :: SqlFragment noLocationF :: SqlFragment
noLocationF = "array[]::text[]" noLocationF = "array[]::text[]"
@@ -117,20 +123,24 @@ pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment
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 -> SqlFragment pgFmtField :: QualifiedIdentifier -> Field -> H.Snippet
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp pgFmtField table (c, jp) = H.sql (pgFmtColumn table c) <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> H.Snippet
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs fName jp alias pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> H.sql (pgFmtAs fName jp alias)
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> encodeUtf8 cast <> " )" <> pgFmtAs fName jp alias -- 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.
-- Not quoting should be fine, we validate the input on Parsers.
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> H.sql (encodeUtf8 cast) <> " )" <> H.sql (pgFmtAs fName jp alias)
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SqlFragment pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> H.Snippet
pgFmtOrderTerm qi ot = BS.unwords [ pgFmtOrderTerm qi ot =
pgFmtField qi $ otTerm ot, pgFmtField qi (otTerm ot) <> " " <>
H.sql (BS.unwords [
BS.pack $ maybe mempty show $ otDirection ot, BS.pack $ maybe mempty show $ otDirection ot,
BS.pack $ maybe mempty show $ otNullOrder ot] BS.pack $ maybe mempty show $ otNullOrder ot])
pgFmtFilter :: QualifiedIdentifier -> Filter -> SqlFragment pgFmtFilter :: QualifiedIdentifier -> Filter -> H.Snippet
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of
Op op val -> pgFmtFieldOp op <> " " <> case op of Op op val -> pgFmtFieldOp op <> " " <> case op of
"like" -> unknownLiteral (T.map star val) "like" -> unknownLiteral (T.map star val)
@@ -138,47 +148,49 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
"is" -> whiteList val "is" -> whiteList val
_ -> unknownLiteral val _ -> unknownLiteral val
-- We don't use "IN", we use "= ANY". IN has the following disadvantages:
-- + No way to use an empty value on IN: "col IN ()" is invalid syntax. With ANY we can do "= ANY('{}')"
-- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching.
In vals -> pgFmtField table fld <> " " <> In vals -> pgFmtField table fld <> " " <>
let emptyValForIn = "= any('{}') " in -- Workaround because for postgresql "col IN ()" is invalid syntax, we instead do "col = any('{}')" case vals of
case (&&) (length vals == 1) . T.null <$> headMay vals of [""] -> "= ANY('{}') "
Just False -> sqlOperator "in" <> "(" <> BS.intercalate ", " (unknownLiteral <$> vals) <> ") " -- Here we build the pg array, e.g '{"Hebdon, John","Other","Another"}', manually. We quote the values to prevent the "," being treated as an element separator.
Just True -> emptyValForIn -- TODO: Ideally this would be done on Hasql with an encoder, but the "array unknown" is not working(Hasql doesn't pass any value).
Nothing -> emptyValForIn _ -> "= ANY (" <> unknownLiteral ("{" <> T.intercalate "," ((\x -> "\"" <> x <> "\"") <$> vals) <> "}") <> ")"
Fts op lang val -> Fts op lang val ->
pgFmtFieldOp op pgFmtFieldOp op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
<> "("
<> maybe mempty ((<> ", ") . pgFmtLit) lang
<> unknownLiteral val
<> ") "
where where
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op
sqlOperator o = HM.lookupDefault "=" o operators sqlOperator o = H.sql $ HM.lookupDefault "=" o operators
notOp = if hasNot then "NOT" else mempty notOp = if hasNot then "NOT" else mempty
star c = if c == '*' then '%' else c star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit -- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
whiteList :: Text -> SqlFragment -- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
whiteList v = maybe -- However that would not accept the TRUE/FALSE/NULL keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
whiteList :: Text -> H.Snippet
whiteList v = H.sql $ maybe
(pgFmtLit v <> "::unknown") encodeUtf8 (pgFmtLit v <> "::unknown") encodeUtf8
(find ((==) . T.toLower $ v) ["null","true","false"]) (find ((==) . T.toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SqlFragment pgFmtJoinCondition :: JoinCondition -> H.Snippet
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) = pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2 H.sql $ pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> H.Snippet
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> BS.intercalate (" " <> BS.pack (show op) <> " ") (pgFmtLogicTree qi <$> forest) <> ")" pgFmtLogicTree qi (Expr hasNot op forest) = H.sql notOp <> " (" <> intercalateSnippet (" " <> BS.pack (show op) <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
where notOp = if hasNot then "NOT" else mempty where notOp = if hasNot then "NOT" else mempty
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
pgFmtJsonPath :: JsonPath -> SqlFragment pgFmtJsonPath :: JsonPath -> H.Snippet
pgFmtJsonPath = \case pgFmtJsonPath = \case
[] -> mempty [] -> mempty
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs (JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs (J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
where where
pgFmtJsonOperand (JKey k) = pgFmtLit k pgFmtJsonOperand (JKey k) = unknownLiteral k
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int" pgFmtJsonOperand (JIdx i) = unknownLiteral i <> "::int"
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
pgFmtAs _ [] Nothing = mempty pgFmtAs _ [] Nothing = mempty
@@ -192,7 +204,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 :: SqlQuery -> Bool -> (SqlFragment, SqlFragment) countF :: H.Snippet -> Bool -> (H.Snippet, SqlFragment)
countF countQuery shouldCount = countF countQuery shouldCount =
if shouldCount if shouldCount
then ( then (
@@ -208,6 +220,13 @@ returningF qi 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 " <> BS.intercalate ", " (pgFmtColumn qi <$> returnings)
limitOffsetF :: NonnegRange -> H.Snippet
limitOffsetF range =
("LIMIT " <> limit <> " OFFSET " <> offset) `emptySnippetOnFalse` (range == allRange)
where
limit = maybe "ALL" (\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range
offset = unknownEncoder (BS.pack . show $ rangeOffset range)
responseHeadersF :: PgVersion -> SqlFragment responseHeadersF :: PgVersion -> SqlFragment
responseHeadersF pgVer = responseHeadersF pgVer =
if pgVer >= pgVersion96 if pgVer >= pgVersion96
@@ -224,3 +243,17 @@ currentSettingF :: Text -> SqlFragment
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(" <> pgFmtLit setting <> ", true), '')" "nullif(current_setting(" <> pgFmtLit setting <> ", true), '')"
-- Hasql Snippet utilitarians
unknownEncoder :: ByteString -> H.Snippet
unknownEncoder = H.encoderAndParam (HE.nonNullable HE.unknown)
unknownLiteral :: Text -> H.Snippet
unknownLiteral = unknownEncoder . encodeUtf8
emptySnippetOnFalse :: H.Snippet -> Bool -> H.Snippet
emptySnippetOnFalse val cond = if cond then mempty else val
intercalateSnippet :: SqlFragment -> [H.Snippet] -> H.Snippet
intercalateSnippet _ [] = mempty
intercalateSnippet frag snippets = foldr1 (\a b -> a <> H.sql frag <> b) snippets
+34 -46
View File
@@ -27,59 +27,54 @@ import qualified Hasql.DynamicStatements.Snippet as H
import Data.Tree (Tree (..)) import Data.Tree (Tree (..))
import Data.Maybe import Data.Maybe
import PostgREST.Private.QueryFragment import PostgREST.Private.QueryFragment
import PostgREST.RangeQuery (allRange, rangeLimit,
rangeOffset)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (cast, intercalate, import Protolude hiding (cast, intercalate,
replace) replace)
readRequestToQuery :: ReadRequest -> SqlQuery readRequestToQuery :: ReadRequest -> H.Snippet
readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) = readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
BS.unwords [ "SELECT " <>
"SELECT " <> BS.intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), intercalateSnippet ", " ((pgFmtSelectItem qi <$> colSelects) ++ selects) <>
"FROM " <> BS.intercalate ", " (tabl : implJs), "FROM " <> H.sql (BS.intercalate ", " (tabl : implJs)) <> " " <>
BS.unwords joins, intercalateSnippet " " joins <> " " <>
("WHERE " <> BS.intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_)) ("WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_))
`emptyOnFalse` (null logicForest && null joinConditions_), `emptySnippetOnFalse` (null logicForest && null joinConditions_) <> " " <>
("ORDER BY " <> BS.intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts, (("ORDER BY " <> intercalateSnippet ", " (map (pgFmtOrderTerm qi) ordts)) `emptySnippetOnFalse` null ordts) <> " " <>
("LIMIT " <> maybe "ALL" (BS.pack . show) (rangeLimit range) <> " OFFSET " <> (BS.pack . show) (rangeOffset range)) `emptyOnFalse` (range == allRange) limitOffsetF range
]
where where
implJs = fromQi <$> implJoins implJs = fromQi <$> implJoins
tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias
qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
(joins, selects) = foldr getJoinsSelects ([],[]) forest (joins, selects) = foldr getJoinsSelects ([],[]) forest
getJoinsSelects :: ReadRequest -> ([SqlFragment], [SqlFragment]) -> ([SqlFragment], [SqlFragment]) getJoinsSelects :: ReadRequest -> ([H.Snippet], [H.Snippet]) -> ([H.Snippet], [H.Snippet])
getJoinsSelects rr@(Node (_, (name, Just Relation{relType=relTyp,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) = getJoinsSelects rr@(Node (_, (name, Just Relation{relType=relTyp,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) =
let subquery = readRequestToQuery rr in let subquery = readRequestToQuery rr in
case relTyp of case relTyp of
M2O -> M2O ->
let aliasOrName = fromMaybe name alias let aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName sel = H.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE " in joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> H.sql localTableName <> " ON TRUE " in
(joi:j,sel:s) (joi:j,sel:s)
_ -> _ ->
let sel = "COALESCE ((" let sel = "COALESCE (("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) " <> "SELECT json_agg(" <> H.sql (pgFmtIdent table) <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "FROM (" <> subquery <> ") " <> H.sql (pgFmtIdent table) <> " "
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) in <> "), '[]') AS " <> H.sql (pgFmtIdent (fromMaybe name alias)) in
(j,sel:s) (j,sel:s)
getJoinsSelects (Node (_, (_, Nothing, _, _, _)) _) _ = ([], []) getJoinsSelects (Node (_, (_, Nothing, _, _, _)) _) _ = ([], [])
mutateRequestToQuery :: MutateRequest -> H.Snippet mutateRequestToQuery :: MutateRequest -> H.Snippet
mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) = mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) =
"WITH " <> normalizedBody body <> "WITH " <> normalizedBody body <> " " <>
H.sql (BS.unwords [ "INSERT INTO " <> H.sql (fromQi mainQi) <> H.sql (if S.null iCols then " " else "(" <> cols <> ") ") <>
"INSERT INTO ", fromQi mainQi, if S.null iCols then " " else "(" <> cols <> ")", "SELECT " <> H.sql cols <> " " <>
BS.unwords [ H.sql ("FROM json_populate_recordset (null::" <> fromQi mainQi <> ", " <> selectBody <> ") _ ") <>
"SELECT " <> cols <> " FROM",
"json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ") _",
-- Only used for PUT -- Only used for PUT
("WHERE " <> BS.intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) `emptyOnFalse` null putConditions], ("WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) `emptySnippetOnFalse` null putConditions <>
H.sql (BS.unwords [
maybe "" (\(oncDo, oncCols) -> ( maybe "" (\(oncDo, oncCols) -> (
"ON CONFLICT(" <> BS.intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of "ON CONFLICT(" <> BS.intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of
IgnoreDuplicates -> IgnoreDuplicates ->
@@ -100,13 +95,11 @@ mutateRequestToQuery (Update mainQi uCols body logicForest returnings) =
-- 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
then H.sql ("SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false") then H.sql ("SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false")
else else
"WITH " <> normalizedBody body <> "WITH " <> normalizedBody body <> " " <>
H.sql (BS.unwords [ "UPDATE " <> H.sql (fromQi mainQi) <> " SET " <> H.sql cols <> " " <>
"UPDATE " <> fromQi mainQi <> " SET " <> cols, "FROM (SELECT * FROM json_populate_recordset (null::" <> H.sql (fromQi mainQi) <> " , " <> H.sql selectBody <> " )) _ " <>
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ")) _ ", ("WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)) `emptySnippetOnFalse` null logicForest <> " " <>
("WHERE " <> BS.intercalate " AND " (pgFmtLogicTree mainQi <$> logicForest)) `emptyOnFalse` null logicForest, H.sql (returningF mainQi returnings)
returningF mainQi returnings
])
where where
cols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols) cols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
emptyBodyReturnedColumns :: SqlFragment emptyBodyReturnedColumns :: SqlFragment
@@ -114,11 +107,9 @@ mutateRequestToQuery (Update mainQi uCols body logicForest returnings) =
| null returnings = "NULL" | null returnings = "NULL"
| otherwise = BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) | otherwise = BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
mutateRequestToQuery (Delete mainQi logicForest returnings) = mutateRequestToQuery (Delete mainQi logicForest returnings) =
H.sql $ BS.unwords [ "DELETE FROM " <> H.sql (fromQi mainQi) <> " " <>
"DELETE FROM ", fromQi mainQi, ("WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree mainQi) logicForest)) `emptySnippetOnFalse` null logicForest <> " " <>
("WHERE " <> BS.intercalate " AND " (map (pgFmtLogicTree mainQi) logicForest)) `emptyOnFalse` null logicForest, H.sql (returningF mainQi returnings)
returningF mainQi returnings
]
requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Maybe PayloadJSON -> Bool -> Maybe PreferParameters -> [FieldName] -> H.Snippet requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Maybe PayloadJSON -> Bool -> Maybe PreferParameters -> [FieldName] -> H.Snippet
requestToCallProcQuery qi pgArgs pj returnsScalar preferParams returnings = requestToCallProcQuery qi pgArgs pj returnsScalar preferParams returnings =
@@ -174,16 +165,13 @@ requestToCallProcQuery qi pgArgs pj returnsScalar preferParams returnings =
-- 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.
-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns) -- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)
-- inside the FROM target. -- inside the FROM target.
readRequestToCountQuery :: ReadRequest -> SqlQuery readRequestToCountQuery :: ReadRequest -> H.Snippet
readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) = readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) =
BS.unwords [ "SELECT 1 " <> "FROM " <> H.sql (fromQi qi) <> " " <>
"SELECT 1", ("WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest)) `emptySnippetOnFalse` null logicForest
"FROM " <> fromQi qi,
("WHERE " <> BS.intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest
]
limitedQuery :: SqlQuery -> Maybe Integer -> SqlQuery limitedQuery :: H.Snippet -> Maybe Integer -> H.Snippet
limitedQuery query maxRows = query <> maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows limitedQuery query maxRows = query <> H.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
setLocalQuery :: Text -> (Text, Text) -> SqlQuery setLocalQuery :: Text -> (Text, Text) -> SqlQuery
setLocalQuery prefix (k, v) = setLocalQuery prefix (k, v) =
+25 -27
View File
@@ -24,7 +24,6 @@ import qualified Data.ByteString.Char8 as BS
import Data.Maybe import Data.Maybe
import Data.Text.Read (decimal) import Data.Text.Read (decimal)
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H import qualified Hasql.Statement as H
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import PostgREST.Error import PostgREST.Error
@@ -34,7 +33,6 @@ import PostgREST.Types
import Protolude hiding (cast, import Protolude hiding (cast,
replace, toS) replace, toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
import Text.InterpolatedString.Perl6 (qc)
import qualified Hasql.DynamicStatements.Snippet as H import qualified Hasql.DynamicStatements.Snippet as H
import qualified Hasql.DynamicStatements.Statement as H import qualified Hasql.DynamicStatements.Statement as H
@@ -45,7 +43,7 @@ import qualified Hasql.DynamicStatements.Statement as H
-} -}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status)) type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status))
createWriteStatement :: SqlQuery -> H.Snippet -> Bool -> Bool -> Bool -> createWriteStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> PgVersion -> PreferRepresentation -> [Text] -> PgVersion ->
H.Statement () ResultsWithCount H.Statement () ResultsWithCount
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys pgVer = createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys pgVer =
@@ -60,9 +58,9 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
locF <> " AS header, " <> locF <> " AS header, " <>
bodyF <> " AS body, " <> bodyF <> " AS body, " <>
responseHeadersF pgVer <> " AS response_headers, " <> responseHeadersF pgVer <> " AS response_headers, " <>
responseStatusF pgVer <> " AS response_status " <> responseStatusF pgVer <> " AS response_status "
) <>
"FROM (" <> selectF <> ") _postgrest_t" "FROM (" <> selectF <> ") _postgrest_t"
)
locF = locF =
if isInsert && rep `elem` [Full, HeadersOnly] if isInsert && rep `elem` [Full, HeadersOnly]
@@ -81,30 +79,30 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv 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 `elem` [None, HeadersOnly] = "SELECT * FROM " <> sourceCTEName | rep `elem` [None, HeadersOnly] = H.sql ("SELECT * FROM " <> sourceCTEName)
| otherwise = selectQuery | otherwise = selectQuery
decodeStandard :: HD.Result ResultsWithCount decodeStandard :: HD.Result ResultsWithCount
decodeStandard = decodeStandard =
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> createReadStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement () ResultsWithCount H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField pgVer = createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField pgVer =
H.Statement sql HE.noParams decodeStandard False H.dynamicallyParameterized snippet decodeStandard True
where where
sql = [qc| snippet =
WITH "WITH " <>
{sourceCTEName} AS ({selectQuery}) H.sql sourceCTEName <> " AS ( " <> selectQuery <> " ) " <>
{countCTEF} countCTEF <> " " <>
SELECT H.sql ("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, " <>
{noLocationF} AS header, noLocationF <> " AS header, " <>
{bodyF} AS body, bodyF <> " AS body, " <>
{responseHeadersF pgVer} AS response_headers, responseHeadersF pgVer <> " AS response_headers, " <>
{responseStatusF pgVer} AS response_status responseStatusF pgVer <> " AS response_status " <>
FROM ( SELECT * FROM {sourceCTEName}) _postgrest_t |] "FROM ( SELECT * FROM " <> sourceCTEName <> " ) _postgrest_t")
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal
@@ -130,7 +128,7 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
type ProcResults = (Maybe Int64, Int64, ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status)) type ProcResults = (Maybe Int64, Int64, ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status))
callProcStatement :: Bool -> H.Snippet -> SqlQuery -> SqlQuery -> Bool -> callProcStatement :: Bool -> H.Snippet -> H.Snippet -> H.Snippet -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion ->
H.Statement () ProcResults H.Statement () ProcResults
callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal isSingle asCsv asBinary multObjects binaryField pgVer = callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal isSingle asCsv asBinary multObjects binaryField pgVer =
@@ -138,15 +136,15 @@ callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal
where where
snippet = snippet =
"WITH " <> H.sql sourceCTEName <> " AS (" <> callProcQuery <> ") " <> "WITH " <> H.sql sourceCTEName <> " AS (" <> callProcQuery <> ") " <>
H.sql (
countCTEF <> countCTEF <>
H.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 pgVer <> " AS response_headers, " <> responseHeadersF pgVer <> " AS response_headers, " <>
responseStatusF pgVer <> " AS response_status " <> responseStatusF pgVer <> " AS response_status ") <>
"FROM (" <> selectQuery <> ") _postgrest_t") "FROM (" <> selectQuery <> ") _postgrest_t"
(countCTEF, countResultF) = countF countQuery countTotal (countCTEF, countResultF) = countF countQuery countTotal
@@ -173,11 +171,11 @@ callProcStatement returnsScalar callProcQuery selectQuery countQuery countTotal
<*> (fromMaybe defGucHeaders <$> nullableColumn decodeGucHeaders) <*> (fromMaybe defGucHeaders <$> nullableColumn decodeGucHeaders)
<*> (fromMaybe defGucStatus <$> nullableColumn decodeGucStatus) <*> (fromMaybe defGucStatus <$> nullableColumn decodeGucStatus)
createExplainStatement :: SqlQuery -> H.Statement () (Maybe Int64) createExplainStatement :: H.Snippet -> H.Statement () (Maybe Int64)
createExplainStatement countQuery = createExplainStatement countQuery =
H.Statement sql HE.noParams decodeExplain False H.dynamicallyParameterized snippet decodeExplain True
where where
sql = [qc| EXPLAIN (FORMAT JSON) {countQuery} |] snippet = "EXPLAIN (FORMAT JSON) " <> countQuery
-- | -- |
-- An `EXPLAIN (FORMAT JSON) select * from items;` output looks like this: -- An `EXPLAIN (FORMAT JSON) select * from items;` output looks like this:
-- [{ -- [{
+9 -9
View File
@@ -846,7 +846,7 @@ spec actualPgVersion = do
[json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |] [json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith` get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`
[json| [{"name":"David White"},{"name":"Larry Thompson"}] |] [json| [{"name":"David White"},{"name":"Larry Thompson"},{"name":"Double O Seven(007)"}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "succeeds w/ and w/o quoted values" $ do it "succeeds w/ and w/o quoted values" $ do
@@ -854,16 +854,16 @@ spec actualPgVersion = do
[json| [{"name":"Hebdon, John"},{"name":"David White"}] |] [json| [{"name":"Hebdon, John"},{"name":"David White"}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")" `shouldRespondWith` get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")" `shouldRespondWith`
[json| [{"name":"Williams, Mary"},{"name":"David White"}] |] [json| [{"name":"Williams, Mary"},{"name":"David White"},{"name":"Double O Seven(007)"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=in.(\"Double O Seven(007)\")" `shouldRespondWith`
[json| [{"name":"Double O Seven(007)"}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "checks well formed quoted values" $ do it "fails on malformed quoted values" $ do
get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\")" `shouldRespondWith` get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\")" `shouldRespondWith` 400
[json| [] |] { matchHeaders = [matchContentTypeJson] } get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\"\"Mary)" `shouldRespondWith` 400
get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\"\"Mary)" `shouldRespondWith` get "/w_or_wo_comma_names?name=in.(Williams\"Hebdon, John\")" `shouldRespondWith` 400
[json| [] |] { matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=in.(Williams\"Hebdon, John\")" `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
describe "IN and NOT IN empty set" $ do describe "IN and NOT IN empty set" $ do
context "returns an empty result for IN when no value is present" $ do context "returns an empty result for IN when no value is present" $ do
+1
View File
@@ -344,6 +344,7 @@ INSERT INTO w_or_wo_comma_names VALUES ('Williams, Mary');
INSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph'); INSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph');
INSERT INTO w_or_wo_comma_names VALUES ('David White'); INSERT INTO w_or_wo_comma_names VALUES ('David White');
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson'); INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
INSERT INTO w_or_wo_comma_names VALUES ('Double O Seven(007)');
TRUNCATE TABLE items_with_different_col_types CASCADE; TRUNCATE TABLE items_with_different_col_types CASCADE;
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null); INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);