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:
steve-chavez
2026-08-04 13:41:40 -05:00
committed by Steve Chavez
parent a41396c425
commit 932c4f6328
12 changed files with 74 additions and 1323 deletions
+4
View File
@@ -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 - 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 - 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 - 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 ### 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 a static executable for aarch64-linux by @wolfgangwalther in #4193
- Build the minimal docker image 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 - 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 #### Changed Syntax for JWT Role Extraction
-1
View File
@@ -74,5 +74,4 @@ Provides the ``schema_cache`` endpoint that prints the runtime :ref:`schema_cach
"dbRepresentations": ["..."], "dbRepresentations": ["..."],
"dbRoutines": ["..."], "dbRoutines": ["..."],
"dbTables": ["..."], "dbTables": ["..."],
"dbTimezones": ["..."]
} }
+5 -24
View File
@@ -62,8 +62,11 @@ The server ignores unrecognized or unfulfillable preferences by default. You can
Timezone 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 .. 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"} {"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 .. code-block:: bash
curl -i "http://localhost:3000/timestamps" \ curl -i "http://localhost:3000/timestamps" \
-H "Prefer: timezone=Jupiter/Red_Spot" -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 .. code-block:: http
HTTP/1.1 400 Bad Request HTTP/1.1 400 Bad Request
+2 -3
View File
@@ -33,7 +33,6 @@ import PostgREST.ApiRequest.Types (Action (..), DbAction (..),
Payload (..), RequestBody, Payload (..), RequestBody,
Resource (..)) Resource (..))
import PostgREST.Config (AppConfig (..), OpenAPIMode (..)) import PostgREST.Config (AppConfig (..), OpenAPIMode (..))
import PostgREST.Config.Database (TimezoneNames)
import PostgREST.Error (ApiRequestError (..), RangeError (..)) import PostgREST.Error (ApiRequestError (..), RangeError (..))
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.RangeQuery (NonnegRange, allRange, 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} actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
-- | Parses the Prefer header -- | Parses the Prefer header
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences userPreferences :: AppConfig -> Request -> Preferences.Preferences
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req userPreferences conf req = Preferences.fromHeaders (configDbTxAllowOverride conf) $ requestHeaders req
-- | Obtains the Bearer Auth -- | Obtains the Bearer Auth
userBearerAuth :: Request -> Maybe ByteString userBearerAuth :: Request -> Maybe ByteString
@@ -26,11 +26,8 @@ module PostgREST.ApiRequest.Preferences
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as S
import qualified Network.HTTP.Types.Header as HTTP import qualified Network.HTTP.Types.Header as HTTP
import PostgREST.Config.Database (TimezoneNames)
import Protolude import Protolude
-- $setup -- $setup
@@ -66,10 +63,8 @@ data Preferences
-- | -- |
-- Parse HTTP headers based on RFC7240[1] to identify 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: -- 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 -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -86,7 +81,7 @@ data Preferences
-- --
-- Multiple headers can also be used: -- 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 -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -102,13 +97,13 @@ data Preferences
-- --
-- If a preference is set more than once, only the first is used: -- 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 -- Just Commit
-- --
-- This is also the case across multiple headers: -- This is also the case across multiple headers:
-- --
-- >>> :{ -- >>> :{
-- preferResolution . fromHeaders True sc $ -- preferResolution . fromHeaders True $
-- [ ("Prefer", "resolution=ignore-duplicates") -- [ ("Prefer", "resolution=ignore-duplicates")
-- , ("Prefer", "resolution=merge-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: -- 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 -- Preferences
-- { preferResolution = Nothing -- { preferResolution = Nothing
-- , preferRepresentation = Just Full -- , preferRepresentation = Just Full
@@ -131,8 +126,8 @@ data Preferences
-- , invalidPrefs = [ "anything" ] -- , invalidPrefs = [ "anything" ]
-- } -- }
-- --
fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences fromHeaders :: Bool -> [HTTP.Header] -> Preferences
fromHeaders allowTxDbOverride acceptedTzNames headers = fromHeaders allowTxDbOverride headers =
Preferences Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates] { preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly] , preferRepresentation = parsePrefs [Full, None, HeadersOnly]
@@ -140,7 +135,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing , preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls] , preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient] , preferHandling = parsePrefs [Strict, Lenient]
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing , preferTimezone = PreferTimezone <$> timezonePref
, preferMaxAffected = PreferMaxAffected <$> maxAffectedPref , preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
, invalidPrefs = filter isUnacceptable prefs , invalidPrefs = filter isUnacceptable prefs
} }
@@ -160,12 +155,11 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
timezonePref = listStripPrefix "timezone=" prefs timezonePref = listStripPrefix "timezone=" prefs
isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
isUnacceptable p = p `notElem` acceptedPrefs && isUnacceptable p = p `notElem` acceptedPrefs &&
(isNothing (BS.stripPrefix "timezone=" p) || not isTimezonePrefAccepted) && isNothing (BS.stripPrefix "timezone=" p) &&
isNothing (BS.stripPrefix "max-affected=" p) isNothing (BS.stripPrefix "max-affected=" p)
parsePrefs :: ToHeaderValue a => [a] -> Maybe a parsePrefs :: ToHeaderValue a => [a] -> Maybe a
+1 -1
View File
@@ -204,7 +204,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
liftIO $ observer SchemaCacheEmptyObs liftIO $ observer SchemaCacheEmptyObs
throwError Error.NoSchemaCacheError throwError Error.NoSchemaCacheError
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache) let prefs = ApiRequest.userPreferences conf req
body <- liftIO $ Wai.strictRequestBody req body <- liftIO $ Wai.strictRequestBody req
-2
View File
@@ -7,7 +7,6 @@ module PostgREST.Config.Database
, queryRoleSettings , queryRoleSettings
, RoleSettings , RoleSettings
, RoleIsolationLvl , RoleIsolationLvl
, TimezoneNames
, toIsolationLevel , toIsolationLevel
) where ) where
@@ -31,7 +30,6 @@ import Protolude
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString)) type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
toIsolationLevel :: Text -> SQL.IsolationLevel toIsolationLevel :: Text -> SQL.IsolationLevel
toIsolationLevel a = case T.toLower a of toIsolationLevel a = case T.toLower a of
+9 -31
View File
@@ -45,7 +45,7 @@ import Data.Functor.Contravariant ((>$<))
import NeatInterpolation (trimming) import NeatInterpolation (trimming)
import PostgREST.Config (AppConfig (..), LogLevel (..)) import PostgREST.Config (AppConfig (..), LogLevel (..))
import PostgREST.Config.Database (TimezoneNames, toIsolationLevel) import PostgREST.Config.Database (toIsolationLevel)
import PostgREST.Config.PgVersion (PgVersion, pgVersion170) import PostgREST.Config.PgVersion (PgVersion, pgVersion170)
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
@@ -78,7 +78,6 @@ data SchemaCache = SchemaCache
, dbRoutines :: RoutineMap , dbRoutines :: RoutineMap
, dbRepresentations :: RepresentationsMap , dbRepresentations :: RepresentationsMap
, dbMediaHandlers :: MediaHandlerMap , dbMediaHandlers :: MediaHandlerMap
, dbTimezones :: TimezoneNames
-- Memoized fuzzy index of table names per schema to support approximate matching -- 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 -- 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 -- Haskell lazy evaluation ensures it's only built on first use and memoized afterwards
@@ -86,24 +85,22 @@ data SchemaCache = SchemaCache
} deriving (Show) } deriving (Show)
instance JSON.ToJSON SchemaCache where 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 "dbTables" .= JSON.toJSON tabs
, "dbRelationships" .= JSON.toJSON rels , "dbRelationships" .= JSON.toJSON rels
, "dbRoutines" .= JSON.toJSON routs , "dbRoutines" .= JSON.toJSON routs
, "dbRepresentations" .= JSON.toJSON reps , "dbRepresentations" .= JSON.toJSON reps
, "dbMediaHandlers" .= JSON.toJSON hdlers , "dbMediaHandlers" .= JSON.toJSON hdlers
, "dbTimezones" .= JSON.toJSON tzs
] ]
showSummary :: SchemaCache -> Text showSummary :: SchemaCache -> Text
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) = showSummary (SchemaCache tbls rels routs reps mediaHdlrs _) =
T.intercalate ", " T.intercalate ", "
[ show (HM.size tbls) <> " Relations" [ show (HM.size tbls) <> " Relations"
, show (HM.size rels) <> " Relationships" , show (HM.size rels) <> " Relationships"
, show (HM.size routs) <> " RPCs" , show (HM.size routs) <> " RPCs"
, show (HM.size reps) <> " Domain Representations" , show (HM.size reps) <> " Domain Representations"
, show (HM.size mediaHdlrs) <> " Media Type Handlers" , 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 -- | 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 cRels <- sqlTimedStmt gucCRels mempty allComputedRels
reps <- sqlTimedStmt gucDReps conf dataRepresentations reps <- sqlTimedStmt gucDReps conf dataRepresentations
mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers
tzones <- sqlTimedStmt gucTzones mempty timezones
for_ configInternalSCQuerySleepSnd (`SQL.statement` sleepCall) -- only used for testing for_ configInternalSCQuerySleepSnd (`SQL.statement` sleepCall) -- only used for testing
qsTime <- qsTime <-
if isLogDebug if isLogDebug
then Just <$> SQL.statement mempty (extractTimings True) then Just <$> SQL.statement mempty extractTimings
else pure Nothing else pure Nothing
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
@@ -178,7 +174,6 @@ querySchemaCache pgVer conf@AppConfig{..} = do
, dbRoutines = funcs , dbRoutines = funcs
, dbRepresentations = reps , dbRepresentations = reps
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones , dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
, dbTimezones = tzones
, dbTablesFuzzyIndex = , dbTablesFuzzyIndex =
-- Only build fuzzy index for schemas with a reasonable number of tables -- 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. , 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 , dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
, dbMediaHandlers = dbMediaHandlers dbStruct , dbMediaHandlers = dbMediaHandlers dbStruct
, dbTimezones = dbTimezones dbStruct
, dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct , dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct
} }
where where
@@ -1108,19 +1102,6 @@ decodeMediaHandlers =
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text) <*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
<*> (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.Value a -> HE.Params a
param = HE.param . HE.nonNullable 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)" 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. -- Extract all the generated timings (see sqlTimedStatement) converting the value to milliseconds.
extractTimings :: Bool -> SQL.Statement () QueryTimings extractTimings :: SQL.Statement () QueryTimings
extractTimings hasTimezones = SQL.Statement sql HE.noParams decodeThem True extractTimings = SQL.Statement sql HE.noParams decodeThem True
where where
qFrag setting = "extract('milliseconds' from current_setting('pgrst." <> setting <> "', false)::interval)::text" qFrag setting = "extract('milliseconds' from current_setting('pgrst." <> setting <> "', false)::interval)::text"
sql = "SELECT " <> BS.intercalate "," sql = "SELECT " <> BS.intercalate ","
[ qFrag gucTbls, qFrag gucKDeps, qFrag gucRels [ qFrag gucTbls, qFrag gucKDeps, qFrag gucRels
, qFrag gucFuncs, qFrag gucCRels, qFrag gucDReps , qFrag gucFuncs, qFrag gucCRels, qFrag gucDReps
, qFrag gucMHdrs, if hasTimezones then qFrag gucTzones else "'0.0'" , qFrag gucMHdrs
] ]
decodeThem :: HD.Result QueryTimings decodeThem :: HD.Result QueryTimings
decodeThem = HD.singleRow $ decodeThem = HD.singleRow $
QueryTimings 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 <*> column HD.text <*> column HD.text <*> column HD.text
<*> column HD.text <*> column HD.text <*> column HD.text
data QueryTimings = QueryTimings data QueryTimings = QueryTimings
{ qtTables :: Text { qtTables :: Text
@@ -1192,7 +1173,6 @@ data QueryTimings = QueryTimings
, qtCRels :: Text , qtCRels :: Text
, qtDReps :: Text , qtDReps :: Text
, qtMHdrs :: Text , qtMHdrs :: Text
, qtTzones :: Text
} deriving (Show) } deriving (Show)
queryTimingsWLabels :: QueryTimings -> [(ByteString, Text)] queryTimingsWLabels :: QueryTimings -> [(ByteString, Text)]
@@ -1204,10 +1184,9 @@ queryTimingsWLabels qt =
, (gucCRels, qtCRels qt) , (gucCRels, qtCRels qt)
, (gucDReps, qtDReps qt) , (gucDReps, qtDReps qt)
, (gucMHdrs, qtMHdrs 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" gucTbls = "tables"
gucKDeps = "keydeps" gucKDeps = "keydeps"
gucRels = "rels" gucRels = "rels"
@@ -1215,4 +1194,3 @@ gucFuncs = "funcs"
gucCRels = "comprels" gucCRels = "comprels"
gucDReps = "dreps" gucDReps = "dreps"
gucMHdrs = "mhandlers" gucMHdrs = "mhandlers"
gucTzones = "tzones"
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -237,7 +237,6 @@ def test_invalid_openapi_mode(invalidopenapimodes, defaultenv):
"dbRepresentations", "dbRepresentations",
"dbRoutines", "dbRoutines",
"dbTables", "dbTables",
"dbTimezones",
], ],
) )
def test_schema_cache_snapshot(baseenv, key, snapshot_yaml): 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], schema_cache[key],
encoding="utf8", encoding="utf8",
allow_unicode=True, allow_unicode=True,
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper, Dumper=ExtraNewLinesDumper,
) )
assert formatted == snapshot_yaml assert formatted == snapshot_yaml
+1 -3
View File
@@ -1002,9 +1002,8 @@ def test_schema_cache_query_timings_log(level, defaultenv):
**defaultenv, **defaultenv,
"PGRST_LOG_LEVEL": level, "PGRST_LOG_LEVEL": level,
} }
# here we also capture the tzones: <value> ms
log_pattern = re.compile( 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: 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": if level == "debug":
assert len(timing_matches) == 1 assert len(timing_matches) == 1
assert float(timing_matches[0].group(1)) > 0
else: else:
assert not timing_matches assert not timing_matches
@@ -9,52 +9,49 @@ import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWithConfig spec :: SpecWithConfig
spec withConfig = withConfig baseCfg $ spec withConfig = withConfig baseCfg $ do
describe "test Prefer: timezone with db-timezone-enabled is true" $ do context "test Prefer: timezone=America/Los_Angeles" $ do
context "test Prefer: timezone=America/Los_Angeles" $ do it "should change timezone with handling=strict" $
it "should change timezone with handling=strict" $ request methodGet "/timestamps"
request methodGet "/timestamps" [("Prefer", "handling=strict, timezone=America/Los_Angeles")]
[("Prefer", "handling=strict, timezone=America/Los_Angeles")] ""
"" `shouldRespondWith`
`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"}]|]
[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
{ matchStatus = 200 , matchHeaders = [matchContentTypeJson
, matchHeaders = [matchContentTypeJson , "Preference-Applied" <:> "handling=strict, timezone=America/Los_Angeles"]}
, "Preference-Applied" <:> "handling=strict, timezone=America/Los_Angeles"]}
it "should change timezone without handling=strict" $ it "should change timezone without handling=strict" $
request methodGet "/timestamps" request methodGet "/timestamps"
[("Prefer", "timezone=America/Los_Angeles")] [("Prefer", "timezone=America/Los_Angeles")]
"" ""
`shouldRespondWith` `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"}]|] [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 { matchStatus = 200
, matchHeaders = [matchContentTypeJson , matchHeaders = [matchContentTypeJson
, "Preference-Applied" <:> "timezone=America/Los_Angeles"] } , "Preference-Applied" <:> "timezone=America/Los_Angeles"] }
context "test Prefer: timezone=Invalid/Timezone" $ do context "test Prefer: timezone=Invalid/Timezone" $ do
it "should throw error with handling=strict" $ it "should throw error without handling" $
request methodGet "/timestamps" request methodGet "/timestamps"
[("Prefer", "handling=strict, timezone=Invalid/Timezone")] [("Prefer", "timezone=Invalid/Timezone")]
"" ""
`shouldRespondWith` `shouldRespondWith`
[json|{"code":"PGRST122","details":"Invalid preferences: timezone=Invalid/Timezone","hint":null,"message":"Invalid preferences given with handling=strict"}|] [json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
{ matchStatus = 400 } { matchStatus = 400 }
it "should return with default timezone without handling or with handling=lenient" $ do it "should throw error with handling=strict" $
request methodGet "/timestamps" request methodGet "/timestamps"
[("Prefer", "timezone=Invalid/Timezone")] [("Prefer", "handling=strict, timezone=Invalid/Timezone")]
"" ""
`shouldRespondWith` `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"}]|] [json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
{ matchStatus = 200 { matchStatus = 400 }
, matchHeaders = [matchContentTypeJson]}
request methodGet "/timestamps" it "should throw error with handling=lenient" $
[("Prefer", "handling=lenient, timezone=Invalid/Timezone")] 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"}]|] `shouldRespondWith`
{ matchStatus = 200 [json|{"code":"22023","details":null,"hint":null,"message":"invalid value for parameter \"TimeZone\": \"Invalid/Timezone\""}|]
, matchHeaders = [matchContentTypeJson { matchStatus = 400 }
, "Preference-Applied" <:> "handling=lenient"]}