perf: no caching for prefer timezone
`Prefer: timezone` no longer requires the schema cache. Previously this required caching `pg_timezone_names` which was slow in some systems. Closes https://github.com/PostgREST/postgrest/issues/5100 and https://github.com/PostgREST/postgrest/issues/4751.
This commit is contained in:
committed by
Steve Chavez
parent
a41396c425
commit
932c4f6328
@@ -17,6 +17,9 @@ All notable changes to this project will be documented in this file. From versio
|
||||
- Add config `server-reuseport` to allow starting multiple PostgREST instances using the same port on supported platforms by @mkleczek in #4703, #4694
|
||||
- Make config `log-level` reloadable by @taimoorzaeem in #5113
|
||||
- Optimize schema cache domain type resolution by using `pg_basetype` on PostgreSQL 17+ by @joelonsql in #4567
|
||||
- `Prefer: timezone` is optimized so it no longer requires the schema cache by @steve-chavez in #5100
|
||||
+ Previously this required caching `pg_timezone_names` which was slow in some systems
|
||||
- `Prefer: timezone` now supports numeric offsets like `05:00` or `-4` by @steve-chavez in #5100
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -39,6 +42,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
|
||||
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
|
||||
- Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984
|
||||
- `Prefer: timezone` no longer complies with `handling=lenient` and instead always fails by @steve-chavez in #5128
|
||||
|
||||
#### Changed Syntax for JWT Role Extraction
|
||||
|
||||
|
||||
@@ -74,5 +74,4 @@ Provides the ``schema_cache`` endpoint that prints the runtime :ref:`schema_cach
|
||||
"dbRepresentations": ["..."],
|
||||
"dbRoutines": ["..."],
|
||||
"dbTables": ["..."],
|
||||
"dbTimezones": ["..."]
|
||||
}
|
||||
|
||||
@@ -62,8 +62,11 @@ The server ignores unrecognized or unfulfillable preferences by default. You can
|
||||
Timezone
|
||||
========
|
||||
|
||||
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all time zones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
|
||||
.. important::
|
||||
|
||||
``handling=lenient`` is ignored for ``timezone``. Invalid time zones always return an error.
|
||||
|
||||
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all time zones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -84,35 +87,13 @@ The ``timezone`` preference allows you to change the `PostgreSQL timezone <https
|
||||
{"t":"2023-10-18T09:37:59.611-07:00"}
|
||||
]
|
||||
|
||||
For an invalid time zone, PostgREST returns values with the default time zone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).
|
||||
For an invalid time zone, PostgREST returns a database error.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"t":"2023-10-18T12:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T14:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T16:37:59.611+00:00"}
|
||||
]
|
||||
|
||||
Note that there's no ``Preference-Applied`` in the response.
|
||||
|
||||
However, with ``handling=strict``, an invalid time zone preference will throw an :ref:`error <pgrst122>`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: handling=strict, timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
@@ -33,7 +33,6 @@ import PostgREST.ApiRequest.Types (Action (..), DbAction (..),
|
||||
Payload (..), RequestBody,
|
||||
Resource (..))
|
||||
import PostgREST.Config (AppConfig (..), OpenAPIMode (..))
|
||||
import PostgREST.Config.Database (TimezoneNames)
|
||||
import PostgREST.Error (ApiRequestError (..), RangeError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
@@ -109,8 +108,8 @@ userApiRequest conf prefs req reqBody = do
|
||||
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
|
||||
|
||||
-- | Parses the Prefer header
|
||||
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences
|
||||
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req
|
||||
userPreferences :: AppConfig -> Request -> Preferences.Preferences
|
||||
userPreferences conf req = Preferences.fromHeaders (configDbTxAllowOverride conf) $ requestHeaders req
|
||||
|
||||
-- | Obtains the Bearer Auth
|
||||
userBearerAuth :: Request -> Maybe ByteString
|
||||
|
||||
@@ -26,11 +26,8 @@ module PostgREST.ApiRequest.Preferences
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Set as S
|
||||
import qualified Network.HTTP.Types.Header as HTTP
|
||||
|
||||
import PostgREST.Config.Database (TimezoneNames)
|
||||
|
||||
import Protolude
|
||||
|
||||
-- $setup
|
||||
@@ -66,10 +63,8 @@ data Preferences
|
||||
-- |
|
||||
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
|
||||
--
|
||||
-- >>> let sc = S.fromList ["America/Los_Angeles"]
|
||||
--
|
||||
-- One header with comma-separated values can be used to set multiple preferences:
|
||||
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
|
||||
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
@@ -86,7 +81,7 @@ data Preferences
|
||||
--
|
||||
-- Multiple headers can also be used:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid"), ("Prefer", "max-affected=5999")]
|
||||
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid"), ("Prefer", "max-affected=5999")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
@@ -102,13 +97,13 @@ data Preferences
|
||||
--
|
||||
-- If a preference is set more than once, only the first is used:
|
||||
--
|
||||
-- >>> preferTransaction $ fromHeaders True sc [("Prefer", "tx=commit, tx=rollback")]
|
||||
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
|
||||
-- Just Commit
|
||||
--
|
||||
-- This is also the case across multiple headers:
|
||||
--
|
||||
-- >>> :{
|
||||
-- preferResolution . fromHeaders True sc $
|
||||
-- preferResolution . fromHeaders True $
|
||||
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||
-- , ("Prefer", "resolution=merge-duplicates")
|
||||
-- ]
|
||||
@@ -118,7 +113,7 @@ data Preferences
|
||||
--
|
||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders True sc [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
|
||||
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Nothing
|
||||
-- , preferRepresentation = Just Full
|
||||
@@ -131,8 +126,8 @@ data Preferences
|
||||
-- , invalidPrefs = [ "anything" ]
|
||||
-- }
|
||||
--
|
||||
fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences
|
||||
fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
|
||||
fromHeaders allowTxDbOverride headers =
|
||||
Preferences
|
||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||
@@ -140,7 +135,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||
, preferHandling = parsePrefs [Strict, Lenient]
|
||||
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
|
||||
, preferTimezone = PreferTimezone <$> timezonePref
|
||||
, preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
|
||||
, invalidPrefs = filter isUnacceptable prefs
|
||||
}
|
||||
@@ -160,12 +155,11 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
|
||||
|
||||
timezonePref = listStripPrefix "timezone=" prefs
|
||||
isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
|
||||
|
||||
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
|
||||
|
||||
isUnacceptable p = p `notElem` acceptedPrefs &&
|
||||
(isNothing (BS.stripPrefix "timezone=" p) || not isTimezonePrefAccepted) &&
|
||||
isNothing (BS.stripPrefix "timezone=" p) &&
|
||||
isNothing (BS.stripPrefix "max-affected=" p)
|
||||
|
||||
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
|
||||
|
||||
@@ -204,7 +204,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
||||
liftIO $ observer SchemaCacheEmptyObs
|
||||
throwError Error.NoSchemaCacheError
|
||||
|
||||
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache)
|
||||
let prefs = ApiRequest.userPreferences conf req
|
||||
|
||||
body <- liftIO $ Wai.strictRequestBody req
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ module PostgREST.Config.Database
|
||||
, queryRoleSettings
|
||||
, RoleSettings
|
||||
, RoleIsolationLvl
|
||||
, TimezoneNames
|
||||
, toIsolationLevel
|
||||
) where
|
||||
|
||||
@@ -31,7 +30,6 @@ import Protolude
|
||||
|
||||
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
|
||||
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
|
||||
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
|
||||
|
||||
toIsolationLevel :: Text -> SQL.IsolationLevel
|
||||
toIsolationLevel a = case T.toLower a of
|
||||
|
||||
@@ -45,7 +45,7 @@ import Data.Functor.Contravariant ((>$<))
|
||||
import NeatInterpolation (trimming)
|
||||
|
||||
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||
import PostgREST.Config.Database (TimezoneNames, toIsolationLevel)
|
||||
import PostgREST.Config.Database (toIsolationLevel)
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion170)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
@@ -78,7 +78,6 @@ data SchemaCache = SchemaCache
|
||||
, dbRoutines :: RoutineMap
|
||||
, dbRepresentations :: RepresentationsMap
|
||||
, dbMediaHandlers :: MediaHandlerMap
|
||||
, dbTimezones :: TimezoneNames
|
||||
-- Memoized fuzzy index of table names per schema to support approximate matching
|
||||
-- Since index construction can be expensive, we build it once and store in the SchemaCache
|
||||
-- Haskell lazy evaluation ensures it's only built on first use and memoized afterwards
|
||||
@@ -86,24 +85,22 @@ data SchemaCache = SchemaCache
|
||||
} deriving (Show)
|
||||
|
||||
instance JSON.ToJSON SchemaCache where
|
||||
toJSON (SchemaCache tabs rels routs reps hdlers tzs _) = JSON.object [
|
||||
toJSON (SchemaCache tabs rels routs reps hdlers _) = JSON.object [
|
||||
"dbTables" .= JSON.toJSON tabs
|
||||
, "dbRelationships" .= JSON.toJSON rels
|
||||
, "dbRoutines" .= JSON.toJSON routs
|
||||
, "dbRepresentations" .= JSON.toJSON reps
|
||||
, "dbMediaHandlers" .= JSON.toJSON hdlers
|
||||
, "dbTimezones" .= JSON.toJSON tzs
|
||||
]
|
||||
|
||||
showSummary :: SchemaCache -> Text
|
||||
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =
|
||||
showSummary (SchemaCache tbls rels routs reps mediaHdlrs _) =
|
||||
T.intercalate ", "
|
||||
[ show (HM.size tbls) <> " Relations"
|
||||
, show (HM.size rels) <> " Relationships"
|
||||
, show (HM.size routs) <> " RPCs"
|
||||
, show (HM.size reps) <> " Domain Representations"
|
||||
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
|
||||
, show (S.size tzs) <> " Timezones"
|
||||
]
|
||||
|
||||
-- | A view foreign key or primary key dependency detected on its source table
|
||||
@@ -160,13 +157,12 @@ querySchemaCache pgVer conf@AppConfig{..} = do
|
||||
cRels <- sqlTimedStmt gucCRels mempty allComputedRels
|
||||
reps <- sqlTimedStmt gucDReps conf dataRepresentations
|
||||
mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers
|
||||
tzones <- sqlTimedStmt gucTzones mempty timezones
|
||||
|
||||
for_ configInternalSCQuerySleepSnd (`SQL.statement` sleepCall) -- only used for testing
|
||||
|
||||
qsTime <-
|
||||
if isLogDebug
|
||||
then Just <$> SQL.statement mempty (extractTimings True)
|
||||
then Just <$> SQL.statement mempty extractTimings
|
||||
else pure Nothing
|
||||
|
||||
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
||||
@@ -178,7 +174,6 @@ querySchemaCache pgVer conf@AppConfig{..} = do
|
||||
, dbRoutines = funcs
|
||||
, dbRepresentations = reps
|
||||
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
|
||||
, dbTimezones = tzones
|
||||
|
||||
, dbTablesFuzzyIndex =
|
||||
-- Only build fuzzy index for schemas with a reasonable number of tables
|
||||
@@ -221,7 +216,6 @@ removeInternal schemas dbStruct =
|
||||
, dbRoutines = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
||||
, dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
|
||||
, dbMediaHandlers = dbMediaHandlers dbStruct
|
||||
, dbTimezones = dbTimezones dbStruct
|
||||
, dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct
|
||||
}
|
||||
where
|
||||
@@ -1108,19 +1102,6 @@ decodeMediaHandlers =
|
||||
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
|
||||
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
|
||||
|
||||
timezones :: SQL.Statement () TimezoneNames
|
||||
timezones = SQL.Statement sql HE.noParams decodeTimezones True
|
||||
where
|
||||
sql = encodeUtf8 $ unlines
|
||||
-- This CTE wrapper is only added for clarifying the query under pg_stat_statements
|
||||
["WITH pgrst_timezones AS ("
|
||||
, " SELECT name FROM pg_timezone_names"
|
||||
, ")"
|
||||
, "SELECT * FROM pgrst_timezones"
|
||||
]
|
||||
decodeTimezones :: HD.Result TimezoneNames
|
||||
decodeTimezones = S.fromList <$> HD.rowList (column HD.text)
|
||||
|
||||
param :: HE.Value a -> HE.Params a
|
||||
param = HE.param . HE.nonNullable
|
||||
|
||||
@@ -1168,21 +1149,21 @@ sqlTimedStatement isLogDebug guc params stmt =
|
||||
eFrag = "select set_config('pgrst." <> guc <> "', (clock_timestamp() - current_setting('pgrst." <> guc <> "', false)::timestamptz)::text, true)"
|
||||
|
||||
-- Extract all the generated timings (see sqlTimedStatement) converting the value to milliseconds.
|
||||
extractTimings :: Bool -> SQL.Statement () QueryTimings
|
||||
extractTimings hasTimezones = SQL.Statement sql HE.noParams decodeThem True
|
||||
extractTimings :: SQL.Statement () QueryTimings
|
||||
extractTimings = SQL.Statement sql HE.noParams decodeThem True
|
||||
where
|
||||
qFrag setting = "extract('milliseconds' from current_setting('pgrst." <> setting <> "', false)::interval)::text"
|
||||
sql = "SELECT " <> BS.intercalate ","
|
||||
[ qFrag gucTbls, qFrag gucKDeps, qFrag gucRels
|
||||
, qFrag gucFuncs, qFrag gucCRels, qFrag gucDReps
|
||||
, qFrag gucMHdrs, if hasTimezones then qFrag gucTzones else "'0.0'"
|
||||
, qFrag gucMHdrs
|
||||
]
|
||||
decodeThem :: HD.Result QueryTimings
|
||||
decodeThem = HD.singleRow $
|
||||
QueryTimings
|
||||
<$> column HD.text <*> column HD.text <*> column HD.text
|
||||
<*> column HD.text <*> column HD.text <*> column HD.text
|
||||
<*> column HD.text <*> column HD.text
|
||||
<*> column HD.text
|
||||
|
||||
data QueryTimings = QueryTimings
|
||||
{ qtTables :: Text
|
||||
@@ -1192,7 +1173,6 @@ data QueryTimings = QueryTimings
|
||||
, qtCRels :: Text
|
||||
, qtDReps :: Text
|
||||
, qtMHdrs :: Text
|
||||
, qtTzones :: Text
|
||||
} deriving (Show)
|
||||
|
||||
queryTimingsWLabels :: QueryTimings -> [(ByteString, Text)]
|
||||
@@ -1204,10 +1184,9 @@ queryTimingsWLabels qt =
|
||||
, (gucCRels, qtCRels qt)
|
||||
, (gucDReps, qtDReps qt)
|
||||
, (gucMHdrs, qtMHdrs qt)
|
||||
, (gucTzones, qtTzones qt)
|
||||
]
|
||||
|
||||
gucTbls, gucKDeps, gucRels, gucFuncs, gucCRels, gucDReps, gucMHdrs, gucTzones :: ByteString
|
||||
gucTbls, gucKDeps, gucRels, gucFuncs, gucCRels, gucDReps, gucMHdrs :: ByteString
|
||||
gucTbls = "tables"
|
||||
gucKDeps = "keydeps"
|
||||
gucRels = "rels"
|
||||
@@ -1215,4 +1194,3 @@ gucFuncs = "funcs"
|
||||
gucCRels = "comprels"
|
||||
gucDReps = "dreps"
|
||||
gucMHdrs = "mhandlers"
|
||||
gucTzones = "tzones"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -237,7 +237,6 @@ def test_invalid_openapi_mode(invalidopenapimodes, defaultenv):
|
||||
"dbRepresentations",
|
||||
"dbRoutines",
|
||||
"dbTables",
|
||||
"dbTimezones",
|
||||
],
|
||||
)
|
||||
def test_schema_cache_snapshot(baseenv, key, snapshot_yaml):
|
||||
@@ -248,7 +247,7 @@ def test_schema_cache_snapshot(baseenv, key, snapshot_yaml):
|
||||
schema_cache[key],
|
||||
encoding="utf8",
|
||||
allow_unicode=True,
|
||||
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper,
|
||||
Dumper=ExtraNewLinesDumper,
|
||||
)
|
||||
assert formatted == snapshot_yaml
|
||||
|
||||
|
||||
+1
-3
@@ -1002,9 +1002,8 @@ def test_schema_cache_query_timings_log(level, defaultenv):
|
||||
**defaultenv,
|
||||
"PGRST_LOG_LEVEL": level,
|
||||
}
|
||||
# here we also capture the tzones: <value> ms
|
||||
log_pattern = re.compile(
|
||||
r".+: tables: [\d.]+ ms, keydeps: [\d.]+ ms, rels: [\d.]+ ms, funcs: [\d.]+ ms, comprels: [\d.]+ ms, dreps: [\d.]+ ms, mhandlers: [\d.]+ ms, tzones: ([\d.]+) ms"
|
||||
r".+: tables: [\d.]+ ms, keydeps: [\d.]+ ms, rels: [\d.]+ ms, funcs: [\d.]+ ms, comprels: [\d.]+ ms, dreps: [\d.]+ ms, mhandlers: [\d.]+ ms"
|
||||
)
|
||||
|
||||
with run(env=env, no_startup_stdout=False) as postgrest:
|
||||
@@ -1015,7 +1014,6 @@ def test_schema_cache_query_timings_log(level, defaultenv):
|
||||
|
||||
if level == "debug":
|
||||
assert len(timing_matches) == 1
|
||||
assert float(timing_matches[0].group(1)) > 0
|
||||
else:
|
||||
assert not timing_matches
|
||||
|
||||
|
||||
@@ -9,52 +9,49 @@ import Protolude hiding (get)
|
||||
import SpecHelper
|
||||
|
||||
spec :: SpecWithConfig
|
||||
spec withConfig = withConfig baseCfg $
|
||||
describe "test Prefer: timezone with db-timezone-enabled is true" $ do
|
||||
context "test Prefer: timezone=America/Los_Angeles" $ do
|
||||
it "should change timezone with handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=strict, timezone=America/Los_Angeles")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson
|
||||
, "Preference-Applied" <:> "handling=strict, timezone=America/Los_Angeles"]}
|
||||
spec withConfig = withConfig baseCfg $ do
|
||||
context "test Prefer: timezone=America/Los_Angeles" $ do
|
||||
it "should change timezone with handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=strict, timezone=America/Los_Angeles")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson
|
||||
, "Preference-Applied" <:> "handling=strict, timezone=America/Los_Angeles"]}
|
||||
|
||||
it "should change timezone without handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "timezone=America/Los_Angeles")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson
|
||||
, "Preference-Applied" <:> "timezone=America/Los_Angeles"] }
|
||||
it "should change timezone without handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "timezone=America/Los_Angeles")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T05:37:59.611-07:00"}, {"t":"2023-10-18T07:37:59.611-07:00"}, {"t":"2023-10-18T09:37:59.611-07:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson
|
||||
, "Preference-Applied" <:> "timezone=America/Los_Angeles"] }
|
||||
|
||||
context "test Prefer: timezone=Invalid/Timezone" $ do
|
||||
it "should throw error with handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=strict, timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|{"code":"PGRST122","details":"Invalid preferences: timezone=Invalid/Timezone","hint":null,"message":"Invalid preferences given with handling=strict"}|]
|
||||
{ matchStatus = 400 }
|
||||
context "test Prefer: timezone=Invalid/Timezone" $ do
|
||||
it "should throw error without handling" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
it "should return with default timezone without handling or with handling=lenient" $ do
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T12:37:59.611+00:00"}, {"t":"2023-10-18T14:37:59.611+00:00"}, {"t":"2023-10-18T16:37:59.611+00:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]}
|
||||
it "should throw error with handling=strict" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=strict, timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=lenient, timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"t":"2023-10-18T12:37:59.611+00:00"}, {"t":"2023-10-18T14:37:59.611+00:00"}, {"t":"2023-10-18T16:37:59.611+00:00"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson
|
||||
, "Preference-Applied" <:> "handling=lenient"]}
|
||||
it "should throw error with handling=lenient" $
|
||||
request methodGet "/timestamps"
|
||||
[("Prefer", "handling=lenient, timezone=Invalid/Timezone")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
Reference in New Issue
Block a user