Maintain backward compatibility on GUC headers feature

If pg version >= 9.6 is detected the feature is enabled,
also all of the 9.6 dependent tests are moved to their own spec.
This commit is contained in:
steve-chavez
2017-10-18 17:41:55 -05:00
committed by Steve Chávez
parent b9a591aecb
commit 188f947437
12 changed files with 170 additions and 129 deletions
+6 -24
View File
@@ -5,13 +5,12 @@ module Main where
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
PgVersion (..),
minimumPgVersion, minimumPgVersion,
prettyVersion, readOptions) prettyVersion, readOptions)
import PostgREST.DbStructure (getDbStructure) import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Error (encodeError) import PostgREST.Error (encodeError)
import PostgREST.OpenAPI (isMalformedProxyUri) import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (DbStructure, Schema) import PostgREST.Types (DbStructure, Schema, PgVersion(..))
import Protolude hiding (hPutStrLn, replace) import Protolude hiding (hPutStrLn, replace)
import Control.Retry (RetryStatus, capDelay, import Control.Retry (RetryStatus, capDelay,
@@ -25,10 +24,7 @@ import Data.String (IsString (..))
import Data.Text (pack, replace, stripPrefix, strip) import Data.Text (pack, replace, stripPrefix, strip)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn) import Data.Text.IO (hPutStrLn)
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import qualified Hasql.Query as H
import qualified Hasql.Session as H import qualified Hasql.Session as H
import Network.Wai.Handler.Warp (defaultSettings, import Network.Wai.Handler.Warp (defaultSettings,
runSettings, setHost, runSettings, setHost,
@@ -40,19 +36,6 @@ import System.IO (BufferMode (..),
import System.Posix.Signals import System.Posix.Signals
#endif #endif
{-|
Used by connectionWorker to know if it should throw an error and kill the
main thread.
-}
isServerVersionSupported :: H.Session Bool
isServerVersionSupported = do
ver <- H.query () pgVersion
return $ ver >= pgvNum minimumPgVersion
where
pgVersion =
H.statement "SELECT current_setting('server_version_num')::integer"
HE.unit (HD.singleRow $ HD.value HD.int4) False
{-| {-|
The purpose of this worker is to fill the refDbStructure created in 'main' The purpose of this worker is to fill the refDbStructure created in 'main'
with the 'DbStructure' returned from calling 'getDbStructure'. This method with the 'DbStructure' returned from calling 'getDbStructure'. This method
@@ -72,7 +55,7 @@ isServerVersionSupported = do
goes back to 1, otherwise it finishes his work successfully. goes back to 1, otherwise it finishes his work successfully.
-} -}
connectionWorker connectionWorker
:: ThreadId -- ^ This thread is killed if 'isServerVersionSupported' returns false :: ThreadId -- ^ This thread is killed if pg version is unsupported
-> P.Pool -- ^ The PostgreSQL connection pool -> P.Pool -- ^ The PostgreSQL connection pool
-> Schema -- ^ Schema PostgREST is serving up -> Schema -- ^ Schema PostgREST is serving up
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure' -> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
@@ -90,13 +73,13 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
connected <- connectingSucceeded pool connected <- connectingSucceeded pool
when connected $ do when connected $ do
result <- P.use pool $ do result <- P.use pool $ do
supported <- isServerVersionSupported actualPgVersion <- getPgVersion
unless supported $ liftIO $ do unless (actualPgVersion >= minimumPgVersion) $ liftIO $ do
hPutStrLn stderr hPutStrLn stderr
("Cannot run in this PostgreSQL version, PostgREST needs at least " ("Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion) <> pgvName minimumPgVersion)
killThread mainTid killThread mainTid
dbStructure <- getDbStructure schema dbStructure <- getDbStructure schema actualPgVersion
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
case result of case result of
Left e -> do Left e -> do
@@ -107,7 +90,6 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
atomicWriteIORef refIsWorkerOn False atomicWriteIORef refIsWorkerOn False
putStrLn ("Connection successful" :: Text) putStrLn ("Connection successful" :: Text)
{-| {-|
Used by 'connectionWorker' to check if the provided db-uri lets Used by 'connectionWorker' to check if the provided db-uri lets
the application access the PostgreSQL database. This method is used the application access the PostgreSQL database. This method is used
+1
View File
@@ -119,6 +119,7 @@ Test-Suite spec
, Feature.DeleteSpec , Feature.DeleteSpec
, Feature.InsertSpec , Feature.InsertSpec
, Feature.NoJwtSpec , Feature.NoJwtSpec
, Feature.PgVersion96Spec
, Feature.ProxySpec , Feature.ProxySpec
, Feature.QueryLimitedSpec , Feature.QueryLimitedSpec
, Feature.QuerySpec , Feature.QuerySpec
+2 -4
View File
@@ -84,12 +84,9 @@ postgrest conf refDbStructure pool worker =
(iTarget apiRequest) (iAction apiRequest) (iTarget apiRequest) (iAction apiRequest)
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
return $ either (pgError authed) identity response return $ either (pgError authed) identity response
when (isResponse503 response) worker when (responseStatus response == status503) worker
respond response respond response
isResponse503 :: Response -> Bool
isResponse503 resp = statusCode (responseStatus resp) == 503
transactionMode :: DbStructure -> Target -> Action -> H.Mode transactionMode :: DbStructure -> Target -> Action -> H.Mode
transactionMode structure target action = transactionMode structure target action =
case action of case action of
@@ -252,6 +249,7 @@ app dbStructure conf apiRequest =
singular paramsAsSingleObject singular paramsAsSingleObject
(contentType == CTTextCSV) (contentType == CTTextCSV)
(contentType == CTOctetStream) _isReadOnly bField (contentType == CTOctetStream) _isReadOnly bField
(pgVersion dbStructure)
let (tableTotal, queryTotal, body, jsonHeaders) = let (tableTotal, queryTotal, body, jsonHeaders) =
fromMaybe (Just 0, 0, "[]", "[]") row fromMaybe (Just 0, 0, "[]", "[]") row
(status, contentRange) = rangeHeader queryTotal tableTotal (status, contentRange) = rangeHeader queryTotal tableTotal
+5 -6
View File
@@ -19,11 +19,12 @@ module PostgREST.Config ( prettyVersion
, readOptions , readOptions
, corsPolicy , corsPolicy
, minimumPgVersion , minimumPgVersion
, PgVersion (..) , pgVersion96
, AppConfig (..) , AppConfig (..)
) )
where where
import PostgREST.Types (PgVersion(..))
import Control.Applicative import Control.Applicative
import Control.Monad (fail) import Control.Monad (fail)
import Control.Lens (preview) import Control.Lens (preview)
@@ -211,11 +212,9 @@ pathParser =
metavar "FILENAME" <> metavar "FILENAME" <>
help "Path to configuration file" help "Path to configuration file"
data PgVersion = PgVersion {
pgvNum :: Int32
, pgvName :: Text
}
-- | Tells the minimum PostgreSQL version required by this version of PostgREST -- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: PgVersion minimumPgVersion :: PgVersion
minimumPgVersion = PgVersion 90300 "9.3" minimumPgVersion = PgVersion 90300 "9.3"
pgVersion96 :: PgVersion
pgVersion96 = PgVersion 90600 "9.6"
+10 -2
View File
@@ -8,6 +8,7 @@ module PostgREST.DbStructure (
, accessibleTables , accessibleTables
, accessibleProcs , accessibleProcs
, schemaDescription , schemaDescription
, getPgVersion
) where ) where
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
@@ -29,8 +30,8 @@ import GHC.Exts (groupWith)
import Protolude import Protolude
import Unsafe (unsafeHead) import Unsafe (unsafeHead)
getDbStructure :: Schema -> H.Session DbStructure getDbStructure :: Schema -> PgVersion -> H.Session DbStructure
getDbStructure schema = do getDbStructure schema pgVer = do
tabs <- H.query () allTables tabs <- H.query () allTables
cols <- H.query () $ allColumns tabs cols <- H.query () $ allColumns tabs
syns <- H.query () $ allSynonyms cols syns <- H.query () $ allSynonyms cols
@@ -48,6 +49,7 @@ getDbStructure schema = do
, dbRelations = rels' , dbRelations = rels'
, dbPrimaryKeys = keys' , dbPrimaryKeys = keys'
, dbProcs = procs , dbProcs = procs
, pgVersion = pgVer
} }
decodeTables :: HD.Result [Table] decodeTables :: HD.Result [Table]
@@ -706,3 +708,9 @@ synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
col1 = findCol s1 t1 c1 col1 = findCol s1 t1 c1
col2 = findCol s2 t2 c2 col2 = findCol s2 t2 c2
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
getPgVersion :: H.Session PgVersion
getPgVersion = H.query () $ H.statement sql HE.unit versionRow False
where
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
versionRow = HD.singleRow $ PgVersion <$> HD.value HD.int4 <*> HD.value HD.text
+10 -6
View File
@@ -31,6 +31,7 @@ import qualified Hasql.Decoders as HD
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import PostgREST.Config (pgVersion96)
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset, allRange) import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset, allRange)
import Data.Functor.Contravariant (contramap) import Data.Functor.Contravariant (contramap)
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
@@ -143,9 +144,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
| otherwise = asJsonF | otherwise = asJsonF
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString) type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> H.Query () (Maybe ProcResults) Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> H.Query () (Maybe ProcResults)
callProc qi params returnsScalar selectQuery countQuery countTotal isSingle paramsAsJson asCsv asBinary isReadOnly binaryField = callProc qi params returnsScalar selectQuery countQuery countTotal isSingle paramsAsJson asCsv asBinary isReadOnly binaryField pgVer =
unicodeStatement sql HE.unit decodeProc True unicodeStatement sql HE.unit decodeProc True
where where
sql = sql =
@@ -155,7 +156,7 @@ callProc qi params returnsScalar selectQuery countQuery countTotal isSingle para
{countResultF} AS total_result_set, {countResultF} AS total_result_set,
1 AS page_total, 1 AS page_total,
{scalarBodyF} AS body, {scalarBodyF} AS body,
{responseHeaders} AS headers {responseHeaders} AS response_headers
FROM ({selectQuery}) _postgrest_t;|] FROM ({selectQuery}) _postgrest_t;|]
else [qc| else [qc|
WITH {sourceCTEName} AS (select * from {fromQi qi}({_args})) WITH {sourceCTEName} AS (select * from {fromQi qi}({_args}))
@@ -163,7 +164,7 @@ callProc qi params returnsScalar selectQuery countQuery countTotal isSingle para
{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,
{responseHeaders} AS headers {responseHeaders} AS response_headers
FROM ({selectQuery}) _postgrest_t;|] FROM ({selectQuery}) _postgrest_t;|]
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
@@ -172,7 +173,10 @@ callProc qi params returnsScalar selectQuery countQuery countTotal isSingle para
else intercalate "," $ map _assignment (HM.toList params) else intercalate "," $ map _assignment (HM.toList params)
_procName = qiName qi _procName = qiName qi
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v _assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
responseHeaders = "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15 responseHeaders =
if pgVer >= pgVersion96
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
else "'[]'" :: Text
decodeProc = HD.maybeRow procRow decodeProc = HD.maybeRow procRow
procRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8 procRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.bytea <*> HD.value HD.bytea <*> HD.value HD.bytea <*> HD.value HD.bytea
+6
View File
@@ -31,6 +31,7 @@ data DbStructure = DbStructure {
, dbRelations :: [Relation] , dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey] , dbPrimaryKeys :: [PrimaryKey]
, dbProcs :: M.HashMap Text ProcDescription , dbProcs :: M.HashMap Text ProcDescription
, pgVersion :: PgVersion
} deriving (Show, Eq) } deriving (Show, Eq)
data PgArg = PgArg { data PgArg = PgArg {
@@ -271,3 +272,8 @@ toMime CTSingularJSON = "application/vnd.pgrst.object+json"
toMime CTOctetStream = "application/octet-stream" toMime CTOctetStream = "application/octet-stream"
toMime CTAny = "*/*" toMime CTAny = "*/*"
toMime (CTOther ct) = ct toMime (CTOther ct) = ct
data PgVersion = PgVersion {
pgvNum :: Int32
, pgvName :: Text
} deriving (Eq, Ord, Show)
+1 -1
View File
@@ -73,7 +73,7 @@ spec =
it "can handle fts" $ do it "can handle fts" $ do
get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith` get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] } [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/tsearch?or=(text_search_vector.phrase.german.fts.Art%20Spass, text_search_vector.plain.french.fts.amusant%20impossible, text_search_vector.english.fts.impossible)" `shouldRespondWith` get "/tsearch?or=(text_search_vector.plain.german.fts.Art%20Spass, text_search_vector.plain.french.fts.amusant%20impossible, text_search_vector.english.fts.impossible)" `shouldRespondWith`
[json|[ [json|[
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }, {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
+99
View File
@@ -0,0 +1,99 @@
module Feature.PgVersion96Spec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "features supported on PostgreSQL 9.6" $ do
context "GUC headers" $ do
it "succeeds setting the headers" $ do
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
`shouldRespondWith` [json|[{"id": 2}]|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
get "/rpc/get_int_and_guc_headers?num=1"
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
it "fails when setting headers with wrong json structure" $ do
get "/rpc/bad_guc_headers_1" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_2" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_3" `shouldRespondWith` 500
post "/rpc/bad_guc_headers_1" [json|{}|] `shouldRespondWith` 500
it "can set the same http header twice" $
get "/rpc/set_cookie_twice"
`shouldRespondWith` "null"
{matchHeaders = [
matchContentTypeJson,
"Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/",
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
context "Use of the phraseto_tsquery function" $ do
it "finds matches" $
get "/tsearch?text_search_vector=phrase.fts.The%20Fat%20Cats" `shouldRespondWith`
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "finds matches with different dictionaries" $
get "/tsearch?text_search_vector=phrase.german.fts.Art%20Spass" `shouldRespondWith`
[json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can be negated with not operator" $
get "/tsearch?text_search_vector=not.phrase.english.fts.The%20Fat%20Cats" `shouldRespondWith`
[json| [
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can be used with or query param" $
get "/tsearch?or=(text_search_vector.phrase.german.fts.Art%20Spass, text_search_vector.phrase.french.fts.amusant, text_search_vector.english.fts.impossible)" `shouldRespondWith`
[json|[
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
]|] { matchHeaders = [matchContentTypeJson] }
-- TODO: remove in 0.5.0 as deprecated
it "Deprecated @@ operator, pending to remove" $ do
get "/tsearch?text_search_vector=phrase.@@.The%20Fat%20Cats" `shouldRespondWith`
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=not.phrase.english.@@.The%20Fat%20Cats" `shouldRespondWith`
[json| [
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
context "GET rpc" $
it "should work with phrase fts" $ do
get "/rpc/get_tsearch?text_search_vector=phrase.english.fts.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
-- TODO: '@@' deprecated
get "/rpc/get_tsearch?text_search_vector=phrase.english.@@.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
-25
View File
@@ -116,11 +116,6 @@ spec = do
[json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "finds matches with phraseto_tsquery" $
get "/tsearch?text_search_vector=phrase.fts.The%20Fat%20Cats" `shouldRespondWith`
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "finds matches with different dictionaries" $ do it "finds matches with different dictionaries" $ do
get "/tsearch?text_search_vector=french.fts.amusant" `shouldRespondWith` get "/tsearch?text_search_vector=french.fts.amusant" `shouldRespondWith`
[json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]
@@ -128,9 +123,6 @@ spec = do
get "/tsearch?text_search_vector=plain.french.fts.amusant%20impossible" `shouldRespondWith` get "/tsearch?text_search_vector=plain.french.fts.amusant%20impossible" `shouldRespondWith`
[json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=phrase.german.fts.Art%20Spass" `shouldRespondWith`
[json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can be negated with not operator" $ do it "can be negated with not operator" $ do
get "/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith` get "/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith`
@@ -150,13 +142,6 @@ spec = do
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=not.phrase.english.fts.The%20Fat%20Cats" `shouldRespondWith`
[json| [
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
-- TODO: remove in 0.5.0 as deprecated -- TODO: remove in 0.5.0 as deprecated
it "Deprecated @@ operator, pending to remove" $ do it "Deprecated @@ operator, pending to remove" $ do
@@ -166,9 +151,6 @@ spec = do
get "/tsearch?text_search_vector=plain.@@.The%20Fat%20Rats" `shouldRespondWith` get "/tsearch?text_search_vector=plain.@@.The%20Fat%20Rats" `shouldRespondWith`
[json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=phrase.@@.The%20Fat%20Cats" `shouldRespondWith`
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=not.@@.impossible%7Cfat%7Cfun" `shouldRespondWith` get "/tsearch?text_search_vector=not.@@.impossible%7Cfat%7Cfun" `shouldRespondWith`
[json| [ [json| [
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
@@ -186,13 +168,6 @@ spec = do
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/tsearch?text_search_vector=not.phrase.english.@@.The%20Fat%20Cats" `shouldRespondWith`
[json| [
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "matches with computed column" $ it "matches with computed column" $
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith` get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
+1 -42
View File
@@ -286,41 +286,6 @@ spec =
it "defaults to status 500 if RAISE code is PT not followed by a number" $ it "defaults to status 500 if RAISE code is PT not followed by a number" $
get "/rpc/raise_bad_pt" `shouldRespondWith` 500 get "/rpc/raise_bad_pt" `shouldRespondWith` 500
context "GUC headers" $ do
it "succeeds setting the headers" $ do
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
`shouldRespondWith` [json|[{"id": 2}]|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
get "/rpc/get_int_and_guc_headers?num=1"
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
it "fails when setting headers with wrong json structure" $ do
get "/rpc/bad_guc_headers_1" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_2" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_3" `shouldRespondWith` 500
post "/rpc/bad_guc_headers_1" [json|{}|] `shouldRespondWith` 500
it "can set the same http header twice" $
get "/rpc/set_cookie_twice"
`shouldRespondWith` "null"
{matchHeaders = [
matchContentTypeJson,
"Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/",
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
context "only for POST rpc" $ do context "only for POST rpc" $ do
context "expects a single json object" $ do context "expects a single json object" $ do
it "does not expand posted json into parameters" $ it "does not expand posted json into parameters" $
@@ -359,16 +324,13 @@ spec =
[json|[{ "id": 2 }, { "id": 4 }]|] [json|[{ "id": 2 }, { "id": 4 }]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "should work with filters that use the plain/phrase with language fts operator" $ do it "should work with filters that use the plain with language fts operator" $ do
get "/rpc/get_tsearch?text_search_vector=english.fts.impossible" `shouldRespondWith` get "/rpc/get_tsearch?text_search_vector=english.fts.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=plain.fts.impossible" `shouldRespondWith` get "/rpc/get_tsearch?text_search_vector=plain.fts.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=phrase.english.fts.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=not.english.fts.fun%7Crat" `shouldRespondWith` get "/rpc/get_tsearch?text_search_vector=not.english.fts.fun%7Crat" `shouldRespondWith`
[json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -379,9 +341,6 @@ spec =
get "/rpc/get_tsearch?text_search_vector=plain.@@.impossible" `shouldRespondWith` get "/rpc/get_tsearch?text_search_vector=plain.@@.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=phrase.english.@@.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=not.english.@@.fun%7Crat" `shouldRespondWith` get "/rpc/get_tsearch?text_search_vector=not.english.@@.fun%7Crat" `shouldRespondWith`
[json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
+29 -19
View File
@@ -5,8 +5,10 @@ import SpecHelper
import qualified Hasql.Pool as P import qualified Hasql.Pool as P
import PostgREST.DbStructure (getDbStructure)
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
import PostgREST.Config (pgVersion96)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Types (DbStructure(..))
import Data.Function (id) import Data.Function (id)
import Data.IORef import Data.IORef
@@ -29,6 +31,7 @@ import qualified Feature.ProxySpec
import qualified Feature.AndOrParamsSpec import qualified Feature.AndOrParamsSpec
import qualified Feature.RpcSpec import qualified Feature.RpcSpec
import qualified Feature.NonexistentSchemaSpec import qualified Feature.NonexistentSchemaSpec
import qualified Feature.PgVersion96Spec
import Protolude import Protolude
@@ -39,8 +42,12 @@ main = do
pool <- P.acquire (3, 10, toS testDbConn) pool <- P.acquire (3, 10, toS testDbConn)
result <- P.use pool $ getDbStructure "test" result <- P.use pool $ getDbStructure "test" =<< getPgVersion
refDbStructure <- newIORef $ Just $ either (panic.show) id result
dbStructure <- pure $ either (panic.show) id result
refDbStructure <- newIORef $ Just dbStructure
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure () let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure ()
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure () ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure ()
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure () unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure ()
@@ -52,6 +59,25 @@ main = do
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure () nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure ()
let reset = resetDb testDbConn let reset = resetDb testDbConn
actualPgVersion = pgVersion dbStructure
pg96spec | actualPgVersion >= pgVersion96 = [("Feature.PgVersion96Spec" , Feature.PgVersion96Spec.spec)]
| otherwise = []
specs = uncurry describe <$> [
("Feature.AuthSpec" , Feature.AuthSpec.spec)
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
, ("Feature.NonexistentSchemaSpec" , Feature.NonexistentSchemaSpec.spec)
] ++ pg96spec
hspec $ do hspec $ do
mapM_ (beforeAll_ reset . before withApp) specs mapM_ (beforeAll_ reset . before withApp) specs
@@ -86,19 +112,3 @@ main = do
-- this test runs with a nonexistent db-schema -- this test runs with a nonexistent db-schema
beforeAll_ reset . before nonexistentSchemaApp $ beforeAll_ reset . before nonexistentSchemaApp $
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
where
specs = map (uncurry describe) [
("Feature.AuthSpec" , Feature.AuthSpec.spec)
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
, ("Feature.NonexistentSchemaSpec" , Feature.NonexistentSchemaSpec.spec)
]