diff --git a/main/Main.hs b/main/Main.hs index 180f93f42..b1ab2696e 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -5,13 +5,12 @@ module Main where import PostgREST.App (postgrest) import PostgREST.Config (AppConfig (..), - PgVersion (..), minimumPgVersion, prettyVersion, readOptions) -import PostgREST.DbStructure (getDbStructure) +import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.Error (encodeError) import PostgREST.OpenAPI (isMalformedProxyUri) -import PostgREST.Types (DbStructure, Schema) +import PostgREST.Types (DbStructure, Schema, PgVersion(..)) import Protolude hiding (hPutStrLn, replace) import Control.Retry (RetryStatus, capDelay, @@ -25,10 +24,7 @@ import Data.String (IsString (..)) import Data.Text (pack, replace, stripPrefix, strip) import Data.Text.Encoding (decodeUtf8, encodeUtf8) 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.Query as H import qualified Hasql.Session as H import Network.Wai.Handler.Warp (defaultSettings, runSettings, setHost, @@ -40,19 +36,6 @@ import System.IO (BufferMode (..), import System.Posix.Signals #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' 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. -} 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 -> Schema -- ^ Schema PostgREST is serving up -> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure' @@ -90,13 +73,13 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do connected <- connectingSucceeded pool when connected $ do result <- P.use pool $ do - supported <- isServerVersionSupported - unless supported $ liftIO $ do + actualPgVersion <- getPgVersion + unless (actualPgVersion >= minimumPgVersion) $ liftIO $ do hPutStrLn stderr ("Cannot run in this PostgreSQL version, PostgREST needs at least " <> pgvName minimumPgVersion) killThread mainTid - dbStructure <- getDbStructure schema + dbStructure <- getDbStructure schema actualPgVersion liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure case result of Left e -> do @@ -107,7 +90,6 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do atomicWriteIORef refIsWorkerOn False putStrLn ("Connection successful" :: Text) - {-| Used by 'connectionWorker' to check if the provided db-uri lets the application access the PostgreSQL database. This method is used diff --git a/postgrest.cabal b/postgrest.cabal index cf1a22e9f..f97709b7b 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -119,6 +119,7 @@ Test-Suite spec , Feature.DeleteSpec , Feature.InsertSpec , Feature.NoJwtSpec + , Feature.PgVersion96Spec , Feature.ProxySpec , Feature.QueryLimitedSpec , Feature.QuerySpec diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 639d3d788..9bb2e0003 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -84,12 +84,9 @@ postgrest conf refDbStructure pool worker = (iTarget apiRequest) (iAction apiRequest) response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq return $ either (pgError authed) identity response - when (isResponse503 response) worker + when (responseStatus response == status503) worker respond response -isResponse503 :: Response -> Bool -isResponse503 resp = statusCode (responseStatus resp) == 503 - transactionMode :: DbStructure -> Target -> Action -> H.Mode transactionMode structure target action = case action of @@ -252,6 +249,7 @@ app dbStructure conf apiRequest = singular paramsAsSingleObject (contentType == CTTextCSV) (contentType == CTOctetStream) _isReadOnly bField + (pgVersion dbStructure) let (tableTotal, queryTotal, body, jsonHeaders) = fromMaybe (Just 0, 0, "[]", "[]") row (status, contentRange) = rangeHeader queryTotal tableTotal diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 5f67cd60f..6ea531190 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -19,11 +19,12 @@ module PostgREST.Config ( prettyVersion , readOptions , corsPolicy , minimumPgVersion - , PgVersion (..) + , pgVersion96 , AppConfig (..) ) where +import PostgREST.Types (PgVersion(..)) import Control.Applicative import Control.Monad (fail) import Control.Lens (preview) @@ -211,11 +212,9 @@ pathParser = metavar "FILENAME" <> help "Path to configuration file" -data PgVersion = PgVersion { - pgvNum :: Int32 -, pgvName :: Text -} - -- | Tells the minimum PostgreSQL version required by this version of PostgREST minimumPgVersion :: PgVersion minimumPgVersion = PgVersion 90300 "9.3" + +pgVersion96 :: PgVersion +pgVersion96 = PgVersion 90600 "9.6" diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 81a528757..2ecbc4518 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -8,6 +8,7 @@ module PostgREST.DbStructure ( , accessibleTables , accessibleProcs , schemaDescription +, getPgVersion ) where import qualified Hasql.Decoders as HD @@ -29,8 +30,8 @@ import GHC.Exts (groupWith) import Protolude import Unsafe (unsafeHead) -getDbStructure :: Schema -> H.Session DbStructure -getDbStructure schema = do +getDbStructure :: Schema -> PgVersion -> H.Session DbStructure +getDbStructure schema pgVer = do tabs <- H.query () allTables cols <- H.query () $ allColumns tabs syns <- H.query () $ allSynonyms cols @@ -48,6 +49,7 @@ getDbStructure schema = do , dbRelations = rels' , dbPrimaryKeys = keys' , dbProcs = procs + , pgVersion = pgVer } decodeTables :: HD.Result [Table] @@ -706,3 +708,9 @@ synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2 col1 = findCol s1 t1 c1 col2 = findCol s2 t2 c2 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 diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index e4121908a..ce977dcdb 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -31,6 +31,7 @@ import qualified Hasql.Decoders as HD import qualified Data.Aeson as JSON +import PostgREST.Config (pgVersion96) import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset, allRange) import Data.Functor.Contravariant (contramap) import qualified Data.HashMap.Strict as HM @@ -143,9 +144,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys | otherwise = asJsonF type ProcResults = (Maybe Int64, Int64, ByteString, ByteString) -callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> - Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> H.Query () (Maybe ProcResults) -callProc qi params returnsScalar selectQuery countQuery countTotal isSingle paramsAsJson asCsv asBinary isReadOnly binaryField = +callProc :: QualifiedIdentifier -> JSON.Object -> Bool -> SqlQuery -> SqlQuery -> Bool -> + 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 pgVer = unicodeStatement sql HE.unit decodeProc True where sql = @@ -155,7 +156,7 @@ callProc qi params returnsScalar selectQuery countQuery countTotal isSingle para {countResultF} AS total_result_set, 1 AS page_total, {scalarBodyF} AS body, - {responseHeaders} AS headers + {responseHeaders} AS response_headers FROM ({selectQuery}) _postgrest_t;|] else [qc| 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, pg_catalog.count(_postgrest_t) AS page_total, {bodyF} AS body, - {responseHeaders} AS headers + {responseHeaders} AS response_headers FROM ({selectQuery}) _postgrest_t;|] 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) _procName = qiName qi _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 procRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8 <*> HD.value HD.bytea <*> HD.value HD.bytea diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 8e94d0252..9ad8a589d 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -31,6 +31,7 @@ data DbStructure = DbStructure { , dbRelations :: [Relation] , dbPrimaryKeys :: [PrimaryKey] , dbProcs :: M.HashMap Text ProcDescription +, pgVersion :: PgVersion } deriving (Show, Eq) data PgArg = PgArg { @@ -271,3 +272,8 @@ toMime CTSingularJSON = "application/vnd.pgrst.object+json" toMime CTOctetStream = "application/octet-stream" toMime CTAny = "*/*" toMime (CTOther ct) = ct + +data PgVersion = PgVersion { + pgvNum :: Int32 +, pgvName :: Text +} deriving (Eq, Ord, Show) diff --git a/test/Feature/AndOrParamsSpec.hs b/test/Feature/AndOrParamsSpec.hs index 26192c251..1e4145fba 100644 --- a/test/Feature/AndOrParamsSpec.hs +++ b/test/Feature/AndOrParamsSpec.hs @@ -73,7 +73,7 @@ spec = it "can handle fts" $ do get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith` [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|[ {"text_search_vector": "'fun':5 'imposs':9 'kind':3" }, {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, diff --git a/test/Feature/PgVersion96Spec.hs b/test/Feature/PgVersion96Spec.hs new file mode 100644 index 000000000..40d8f5640 --- /dev/null +++ b/test/Feature/PgVersion96Spec.hs @@ -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] } diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 6d6aead9d..4f8a41442 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -116,11 +116,6 @@ spec = do [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] { 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 get "/tsearch?text_search_vector=french.fts.amusant" `shouldRespondWith` [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` [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] { 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 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": "'art':4 'spass':5 'unmog':7"}]|] { 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 it "Deprecated @@ operator, pending to remove" $ do @@ -166,9 +151,6 @@ spec = do get "/tsearch?text_search_vector=plain.@@.The%20Fat%20Rats" `shouldRespondWith` [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] { 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` [json| [ {"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": "'art':4 'spass':5 'unmog':7"}]|] { 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" $ get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith` diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 35413feef..3987ddb26 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -286,41 +286,6 @@ spec = it "defaults to status 500 if RAISE code is PT not followed by a number" $ 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 "expects a single json object" $ do it "does not expand posted json into parameters" $ @@ -359,16 +324,13 @@ spec = [json|[{ "id": 2 }, { "id": 4 }]|] { 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` [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] { matchHeaders = [matchContentTypeJson] } get "/rpc/get_tsearch?text_search_vector=plain.fts.impossible" `shouldRespondWith` [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] { 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` [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] { matchHeaders = [matchContentTypeJson] } @@ -379,9 +341,6 @@ spec = get "/rpc/get_tsearch?text_search_vector=plain.@@.impossible" `shouldRespondWith` [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] { 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` [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] { matchHeaders = [matchContentTypeJson] } diff --git a/test/Main.hs b/test/Main.hs index be2911a97..b3c6d9674 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -5,8 +5,10 @@ import SpecHelper import qualified Hasql.Pool as P -import PostgREST.DbStructure (getDbStructure) import PostgREST.App (postgrest) +import PostgREST.Config (pgVersion96) +import PostgREST.DbStructure (getDbStructure, getPgVersion) +import PostgREST.Types (DbStructure(..)) import Data.Function (id) import Data.IORef @@ -29,6 +31,7 @@ import qualified Feature.ProxySpec import qualified Feature.AndOrParamsSpec import qualified Feature.RpcSpec import qualified Feature.NonexistentSchemaSpec +import qualified Feature.PgVersion96Spec import Protolude @@ -39,8 +42,12 @@ main = do pool <- P.acquire (3, 10, toS testDbConn) - result <- P.use pool $ getDbStructure "test" - refDbStructure <- newIORef $ Just $ either (panic.show) id result + result <- P.use pool $ getDbStructure "test" =<< getPgVersion + + dbStructure <- pure $ either (panic.show) id result + + refDbStructure <- newIORef $ Just dbStructure + let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure () ltdApp = return $ postgrest (testLtdRowsCfg 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 () 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 mapM_ (beforeAll_ reset . before withApp) specs @@ -86,19 +112,3 @@ main = do -- this test runs with a nonexistent db-schema beforeAll_ reset . before nonexistentSchemaApp $ 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) - ]