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
+2 -3
View File
@@ -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
+1 -1
View File
@@ -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
-2
View File
@@ -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
+9 -31
View File
@@ -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"