Merge pull request #425 from ruslantalpa/fix_414_revert_count_query_no_cte

#414 revert to separate count query
This commit is contained in:
Joe Nelson
2015-12-15 12:06:19 -08:00
3 changed files with 77 additions and 83 deletions
+1
View File
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix #396 include records with missing parents - @ruslantalpa - Fix #396 include records with missing parents - @ruslantalpa
- `pgFmtIdent` always quotes #388 - @calebmer - `pgFmtIdent` always quotes #388 - @calebmer
- Default schema, changed from `"1"` to `public` - @calebmer - Default schema, changed from `"1"` to `public` - @calebmer
- #414 revert to separate count query
### Added ### Added
- Allow order by computed columns - @diogob - Allow order by computed columns - @diogob
+19 -14
View File
@@ -51,8 +51,9 @@ import PostgREST.Error (errResponse)
import PostgREST.QueryBuilder ( asJson import PostgREST.QueryBuilder ( asJson
, callProc , callProc
, addJoinConditions , addJoinConditions
, sourceSubqueryName , sourceCTEName
, requestToQuery , requestToQuery
, requestToCountQuery
, addRelations , addRelations
, createReadStatement , createReadStatement
, createWriteStatement , createWriteStatement
@@ -70,12 +71,12 @@ app dbStructure conf reqBody req =
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) -> (ActionRead, TargetIdent qi, Nothing) ->
case selectQuery of case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right q -> do Right (q, cq) -> do
let range = restrictRange (configMaxRows conf) $ iRange apiRequest let range = restrictRange (configMaxRows conf) $ iRange apiRequest
singular = iPreferSingular apiRequest singular = iPreferSingular apiRequest
stm = createReadStatement q range singular stm = createReadStatement q cq range singular
(iPreferCount apiRequest) (contentType == TextCSV) (iPreferCount apiRequest) (contentType == TextCSV)
if range == emptyRange if range == emptyRange
then return $ errResponse status416 "HTTP Range error" then return $ errResponse status416 "HTTP Range error"
@@ -106,7 +107,7 @@ app dbStructure conf reqBody req =
(ActionCreate, TargetIdent (QualifiedIdentifier _ table), (ActionCreate, TargetIdent (QualifiedIdentifier _ table),
Just payload@(PayloadJSON (UniformObjects rows))) -> Just payload@(PayloadJSON (UniformObjects rows))) ->
case queries of case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do Right (sq,mq) -> do
let isSingle = (==1) $ V.length rows let isSingle = (==1) $ V.length rows
@@ -122,7 +123,7 @@ app dbStructure conf reqBody req =
$ if iPreferRepresentation apiRequest then fromMaybe "[]" body else "" $ if iPreferRepresentation apiRequest then fromMaybe "[]" body else ""
(ActionUpdate, TargetIdent _, Just payload@(PayloadJSON _)) -> (ActionUpdate, TargetIdent _, Just payload@(PayloadJSON _)) ->
case queries of case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do Right (sq,mq) -> do
let stm = createWriteStatement sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) payload let stm = createWriteStatement sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) payload
@@ -136,7 +137,7 @@ app dbStructure conf reqBody req =
$ if iPreferRepresentation apiRequest then fromMaybe "[]" body else "" $ if iPreferRepresentation apiRequest then fromMaybe "[]" body else ""
(ActionDelete, TargetIdent _, Nothing) -> (ActionDelete, TargetIdent _, Nothing) ->
case queries of case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do Right (sq,mq) -> do
let fakeload = PayloadJSON $ UniformObjects V.empty let fakeload = PayloadJSON $ UniformObjects V.empty
@@ -196,9 +197,13 @@ app dbStructure conf reqBody req =
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
schema = cs $ configSchema conf schema = cs $ configSchema conf
apiRequest = userApiRequest schema req reqBody apiRequest = userApiRequest schema req reqBody
selectQuery = requestToQuery schema <$> (DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest) readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
mutateQuery = requestToQuery schema <$> (DbMutate <$> buildMutateRequest apiRequest) mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
queries = (,) <$> selectQuery <*> mutateQuery selectQuery = requestToQuery schema <$> readDbRequest
countQuery = requestToCountQuery schema <$> readDbRequest
mutateQuery = requestToQuery schema <$> mutateDbRequest
readSqlParts = (,) <$> selectQuery <*> countQuery
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
rangeStatus :: Int -> Int -> Maybe Int -> Status rangeStatus :: Int -> Int -> Maybe Int -> Status
rangeStatus _ _ Nothing = status200 rangeStatus _ _ Nothing = status200
@@ -258,7 +263,7 @@ buildReadRequest allRels apiRequest =
rootName = if action == ActionRead rootName = if action == ActionRead
then rootTableName then rootTableName
else sourceSubqueryName else sourceCTEName
filters = if action == ActionRead filters = if action == ActionRead
then iFilters apiRequest then iFilters apiRequest
else filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update else filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
@@ -313,9 +318,9 @@ addFilter (path, flt) (Node rn forest) =
-- as just another table that has relations with other tables -- as just another table that has relations with other tables
toSourceRelation :: TableName -> Relation -> Maybe Relation toSourceRelation :: TableName -> Relation -> Maybe Relation
toSourceRelation mt r@(Relation t _ ft _ _ rt _ _) toSourceRelation mt r@(Relation t _ ft _ _ rt _ _)
| mt == tableName t = Just $ r {relTable=t {tableName=sourceSubqueryName}} | mt == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| mt == tableName ft = Just $ r {relFTable=t {tableName=sourceSubqueryName}} | mt == tableName ft = Just $ r {relFTable=t {tableName=sourceCTEName}}
| Just mt == (tableName <$> rt) = Just $ r {relLTable=(\tbl -> tbl {tableName=sourceSubqueryName}) <$> rt} | Just mt == (tableName <$> rt) = Just $ r {relLTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt}
| otherwise = Nothing | otherwise = Nothing
data TableOptions = TableOptions { data TableOptions = TableOptions {
+57 -69
View File
@@ -22,7 +22,8 @@ module PostgREST.QueryBuilder (
, pgFmtIdent , pgFmtIdent
, pgFmtLit , pgFmtLit
, requestToQuery , requestToQuery
, sourceSubqueryName , requestToCountQuery
, sourceCTEName
, unquoted , unquoted
) where ) where
@@ -59,18 +60,24 @@ instance Monoid PStmt where
mempty = B.Stmt "" empty True mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt type StatementT = PStmt -> PStmt
createReadStatement :: SqlQuery -> NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
createReadStatement selectQuery range isSingle countTable asCsv = createReadStatement selectQuery countQuery range isSingle countTotal asCsv =
B.Stmt ( B.Stmt (
wrapLimitedQuery selectQuery [ "WITH " <> sourceCTEName <> " AS (" <> selectQuery <> ") " <>
if countTable then countAllF else countNoneF, "SELECT " <> intercalate ", " [
countF, countResultF <> " AS total_result_set",
"null", -- location header can not be calucalted "pg_catalog.count(t) AS page_total",
if asCsv "null AS header",
then asCsvF bodyF <> " AS body"
else if isSingle then asJsonSingleF else asJsonF ] <>
] selectStarF range " FROM ( SELECT * FROM " <> sourceCTEName <> " " <> limitF range <> ") t"
) V.empty True ) V.empty True
where
countResultF = if countTotal then "("<>countQuery<>")" else "null"
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool ->
[Text] -> Bool -> Payload -> B.Stmt P.Postgres [Text] -> Bool -> Payload -> B.Stmt P.Postgres
@@ -78,19 +85,21 @@ createWriteStatement _ _ _ _ _ _ (PayloadParseError _) = undefined
createWriteStatement selectQuery mutateQuery isSingle echoRequested createWriteStatement selectQuery mutateQuery isSingle echoRequested
pKeys asCsv (PayloadJSON (UniformObjects rows)) = pKeys asCsv (PayloadJSON (UniformObjects rows)) =
B.Stmt ( B.Stmt (
wrapQuery mutateQuery [ "WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> ") " <>
countNoneF, -- when updateing it does not make sense "SELECT " <> intercalate ", " [
countF, "null AS total_result_set", -- when updateing it does not make sense
if isSingle then locationF pKeys else "null", "pg_catalog.count(t) AS page_total",
if echoRequested location <> " AS header",
then (if echoRequested then bodyF else "null") <> " AS body"
if asCsv ] <>
then asCsvF " FROM ( "<>selectQuery<>") t"
else if isSingle then asJsonSingleF else asJsonF
else "null"
] selectQuery
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True ) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
where
location = if isSingle then locationF pKeys else "null"
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) forest) = addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) forest) =
@@ -175,6 +184,19 @@ pgFmtLit x =
then "E" <> slashed then "E" <> slashed
else slashed else slashed
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = undefined
requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) _)) =
unwords [
"SELECT pg_catalog.count(1)",
"FROM ", fromQi $ QualifiedIdentifier schema mainTbl,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions
]
where
fn (Filter{value=VText _}) = True
fn (Filter{value=VForeignKey _ _}) = False
localConditions = filter fn conditions
requestToQuery :: Schema -> DbRequest -> SqlQuery requestToQuery :: Schema -> DbRequest -> SqlQuery
requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined
requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined
@@ -183,7 +205,7 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (mai
where where
-- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name -- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name
-- of our WITH query part -- of our WITH query part
tblSchema tbl = if tbl == sourceSubqueryName then "" else schema tblSchema tbl = if tbl == sourceCTEName then "" else schema
qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl
toQi t = QualifiedIdentifier (tblSchema t) t toQi t = QualifiedIdentifier (tblSchema t) t
query = unwords [ query = unwords [
@@ -276,8 +298,8 @@ requestToQuery schema (DbMutate (Delete mainTbl conditions)) =
"RETURNING " <> fromQi qi <> ".*" "RETURNING " <> fromQi qi <> ".*"
] ]
sourceSubqueryName :: SqlFragment sourceCTEName :: SqlFragment
sourceSubqueryName = "pg_source" sourceCTEName = "pg_source"
unquoted :: JSON.Value -> Text unquoted :: JSON.Value -> Text
unquoted (JSON.String t) = t unquoted (JSON.String t) = t
@@ -295,7 +317,7 @@ 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 " <> sourceSubqueryName <> " as hh limit 1" <> " SELECT row_to_json(hh) as r from " <> sourceCTEName <> " as hh limit 1" <>
" ) s" <> " ) s" <>
" ) a" <> " ) a" <>
")" ")"
@@ -307,19 +329,10 @@ asJsonF = "array_to_json(array_agg(row_to_json(t)))::character varying"
asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element
asJsonSingleF = "string_agg(row_to_json(t)::text, ',')::character varying " asJsonSingleF = "string_agg(row_to_json(t)::text, ',')::character varying "
countAllF :: SqlFragment
countAllF = "(SELECT pg_catalog.count(1) FROM (SELECT * FROM " <> sourceSubqueryName <> ") a )"
countF :: SqlFragment
countF = "pg_catalog.count(t)"
countNoneF :: SqlFragment
countNoneF = "null"
locationF :: [Text] -> SqlFragment locationF :: [Text] -> SqlFragment
locationF pKeys = locationF pKeys =
"(" <> "(" <>
" WITH s AS (SELECT row_to_json(ss) as r from " <> sourceSubqueryName <> " as ss limit 1)" <> " WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <>
" SELECT string_agg(json_data.key || '=' || coalesce( 'eq.' || json_data.value, 'is.null'), '&')" <> " SELECT string_agg(json_data.key || '=' || coalesce( 'eq.' || json_data.value, 'is.null'), '&')" <>
" FROM s, json_each_text(s.r) AS json_data" <> " FROM s, json_each_text(s.r) AS json_data" <>
( (
@@ -329,6 +342,12 @@ locationF pKeys =
) <> ) <>
")" ")"
limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
where
limit = maybe "ALL" (cs . show) $ rangeLimit r
offset = cs . show $ rangeOffset r
fromQi :: QualifiedIdentifier -> SqlFragment fromQi :: QualifiedIdentifier -> SqlFragment
fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
where where
@@ -391,7 +410,7 @@ pgFmtCondition table (Filter (col,jp) ops val) =
valToStr v = case v of valToStr v = case v of
VText s -> pgFmtValue opCode s VText s -> pgFmtValue opCode s
VForeignKey (QualifiedIdentifier s _) (ForeignKey Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc VForeignKey (QualifiedIdentifier s _) (ForeignKey Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc
where qi = QualifiedIdentifier (if ft == sourceSubqueryName then "" else s) ft where qi = QualifiedIdentifier (if ft == sourceCTEName then "" else s) ft
_ -> "" _ -> ""
pgFmtValue :: Text -> Text -> SqlFragment pgFmtValue :: Text -> Text -> SqlFragment
@@ -423,34 +442,3 @@ pgFmtAsJsonPath (Just xx) = " AS " <> last xx
trimNullChars :: Text -> Text trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
withSourceF :: SqlFragment -> SqlFragment
withSourceF s = "WITH " <> sourceSubqueryName <> " AS (" <> s <>")"
fromF :: SqlFragment -> SqlFragment -> SqlFragment
fromF sel limit = "FROM (" <> sel <> " " <> limit <> ") t"
limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
where
limit = maybe "ALL" (cs . show) $ rangeLimit r
offset = cs . show $ rangeOffset r
selectStarF :: SqlFragment
selectStarF = "SELECT * FROM " <> sourceSubqueryName
wrapLimitedQuery :: SqlQuery -> [Text] -> Text -> NonnegRange -> SqlQuery
wrapLimitedQuery source selectColumns returnSelect range =
withSourceF source <>
" SELECT " <>
intercalate ", " selectColumns <>
" " <>
fromF returnSelect ( limitF range )
wrapQuery :: SqlQuery -> [Text] -> Text -> SqlQuery
wrapQuery source selectColumns returnSelect =
withSourceF source <>
" SELECT " <>
intercalate ", " selectColumns <>
" " <>
fromF returnSelect ""