From d6c338d588165f098a568c0d3f451ab4edf691cc Mon Sep 17 00:00:00 2001 From: Taimoor Zaeem Date: Wed, 1 Apr 2026 10:46:22 +0500 Subject: [PATCH] add: config db-timezone-enabled for optional querying of timezones To avoid repeated querying of `pg_timezone_names` every time schema cache is reset, `Prefer: timezone` can be disabled by setting `db-timezone-enabled = false`. Signed-off-by: Taimoor Zaeem --- CHANGELOG.md | 1 + docs/references/api/preferences.rst | 4 ++ docs/references/configuration.rst | 15 ++++++++ src/PostgREST/Config.hs | 6 +++ src/PostgREST/Config/Database.hs | 1 + src/PostgREST/SchemaCache.hs | 12 +++++- test/io/configs/expected/aliases.config | 1 + .../configs/expected/boolean-numeric.config | 1 + .../io/configs/expected/boolean-string.config | 1 + test/io/configs/expected/defaults.config | 1 + .../expected/jspath-str-op-dump1.config | 1 + .../expected/jspath-str-op-dump2.config | 1 + .../expected/jspath-str-op-dump3.config | 1 + .../expected/jspath-str-op-dump4.config | 1 + .../expected/jspath-str-op-dump5.config | 1 + ...efaults-with-db-other-authenticator.config | 1 + .../expected/no-defaults-with-db.config | 1 + test/io/configs/expected/no-defaults.config | 1 + test/io/configs/expected/types.config | 1 + test/io/configs/expected/utf-8.config | 1 + test/io/configs/no-defaults-env.yaml | 1 + test/io/configs/no-defaults.config | 1 + test/io/fixtures/db_config.sql | 2 + test/observability/ObsHelper.hs | 1 + .../Feature/Query/Preferences/TimezoneSpec.hs | 37 +++++++++++++++++-- test/spec/Main.hs | 11 +++++- test/spec/SpecHelper.hs | 4 ++ 27 files changed, 104 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a0e5e62..3a61ca5a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. From versio + Removed unnecessary double count when building the `Content-Range`. - Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824 - Add `Vary` header to responses by @develop7 in #4609 +- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751 ### Changed diff --git a/docs/references/api/preferences.rst b/docs/references/api/preferences.rst index 96dd4f48f..66d40229d 100644 --- a/docs/references/api/preferences.rst +++ b/docs/references/api/preferences.rst @@ -117,6 +117,10 @@ However, with ``handling=strict``, an invalid time zone preference will throw an HTTP/1.1 400 Bad Request +.. note:: + + This feature requires querying `pg_timezone_names `_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`. + .. _prefer_return: Return Representation diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index ef56fdd5c..48e1a9275 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -540,6 +540,21 @@ db-schemas The list of database schemas to expose to clients. See :ref:`schemas`. +.. _db-timezone-enabled: + +db-timezone-enabled +------------------- + + =============== ================================= + **Type** Boolean + **Default** True + **Reloadable** Y + **Environment** PGRST_DB_TIMEZONE_ENABLED + **In-Database** pgrst.db_timezone_enabled + =============== ================================= + + Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``. + .. _db-tx-end: db-tx-end diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index fbc685309..d18a8da57 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -94,6 +94,7 @@ data AppConfig = AppConfig , configDbSchemas :: NonEmpty Text , configDbConfig :: Bool , configDbPreConfig :: Maybe QualifiedIdentifier + , configDbTimezoneEnabled :: Bool , configDbTxAllowOverride :: Bool , configDbTxRollbackAll :: Bool , configDbUri :: Text @@ -181,6 +182,7 @@ toText conf = ,("db-schemas", q . T.intercalate "," . toList . configDbSchemas) ,("db-config", T.toLower . show . configDbConfig) ,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig) + ,("db-timezone-enabled", T.toLower . show . configDbTimezoneEnabled) ,("db-tx-end", q . showTxEnd) ,("db-uri", q . configDbUri) ,("jwt-aud", q . fromMaybe mempty . configJwtAudience) @@ -290,6 +292,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl = <*> parseDbSchemas "db-schemas" "db-schema" <*> (fromMaybe True <$> optBool "db-config") <*> (fmap toQi <$> optString "db-pre-config") + <*> (fromMaybe True <$> optBool "db-timezone-enabled") <*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" fst <*> (fromMaybe "postgresql://" <$> optString "db-uri") @@ -715,6 +718,9 @@ exampleConfigFile = S.unlines , "## The name of which database schema to expose to REST clients" , "db-schemas = \"public\"" , "" + , "## Enable quering pg_timezone_names from db" + , "# db-timezone-enabled = true" + , "" , "## How to terminate database transactions" , "## Possible values are:" , "## commit (default)" diff --git a/src/PostgREST/Config/Database.hs b/src/PostgREST/Config/Database.hs index e25cb87f2..7c77f9fdd 100644 --- a/src/PostgREST/Config/Database.hs +++ b/src/PostgREST/Config/Database.hs @@ -56,6 +56,7 @@ dbSettingsNames = ,"db_prepared_statements" ,"db_root_spec" ,"db_schemas" + ,"db_timezone_enabled" ,"db_tx_end" ,"db_hoisted_tx_settings" ,"jwt_aud" diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index f2f859074..c928dac0d 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -159,7 +159,9 @@ querySchemaCache conf@AppConfig{..} = do cRels <- SQL.statement mempty $ allComputedRels prepared reps <- SQL.statement conf $ dataRepresentations prepared mHdlers <- SQL.statement conf $ mediaHandlers prepared - tzones <- SQL.statement mempty $ timezones prepared + tzones <- if configDbTimezoneEnabled + then SQL.statement mempty $ timezones prepared + else pure S.empty _ <- let sleepCall = SQL.Statement "select pg_sleep($1 / 1000.0)" (param HE.int4) HD.noResult prepared in for_ configInternalSCQuerySleep (`SQL.statement` sleepCall) -- only used for testing @@ -1109,7 +1111,13 @@ decodeMediaHandlers = timezones :: Bool -> SQL.Statement () TimezoneNames timezones = SQL.Statement sql HE.noParams decodeTimezones where - sql = "SELECT name FROM pg_timezone_names" + 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) diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index ab6a6f150..d280c6254 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -18,6 +18,7 @@ db-root-spec = "open_alias" db-schemas = "provided_through_alias" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index 22bc20491..7f1ac07a1 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index 22bc20491..7f1ac07a1 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index acbd17508..ddd9364c2 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = false db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/jspath-str-op-dump1.config b/test/io/configs/expected/jspath-str-op-dump1.config index 6e9bc26f4..25fd23346 100644 --- a/test/io/configs/expected/jspath-str-op-dump1.config +++ b/test/io/configs/expected/jspath-str-op-dump1.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/jspath-str-op-dump2.config b/test/io/configs/expected/jspath-str-op-dump2.config index 938ab2f1d..b53bf827e 100644 --- a/test/io/configs/expected/jspath-str-op-dump2.config +++ b/test/io/configs/expected/jspath-str-op-dump2.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/jspath-str-op-dump3.config b/test/io/configs/expected/jspath-str-op-dump3.config index c0fcd2c4b..bd1bed993 100644 --- a/test/io/configs/expected/jspath-str-op-dump3.config +++ b/test/io/configs/expected/jspath-str-op-dump3.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/jspath-str-op-dump4.config b/test/io/configs/expected/jspath-str-op-dump4.config index 3168929b6..b169f03a2 100644 --- a/test/io/configs/expected/jspath-str-op-dump4.config +++ b/test/io/configs/expected/jspath-str-op-dump4.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/jspath-str-op-dump5.config b/test/io/configs/expected/jspath-str-op-dump5.config index b3460f6f6..13596cc81 100644 --- a/test/io/configs/expected/jspath-str-op-dump5.config +++ b/test/io/configs/expected/jspath-str-op-dump5.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index 6896b78e5..c47bb402e 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -18,6 +18,7 @@ db-root-spec = "other_root" db-schemas = "test,other_tenant1,other_tenant2" db-config = true db-pre-config = "postgrest.other_preconf" +db-timezone-enabled = false db-tx-end = "rollback-allow-override" db-uri = "postgresql://" jwt-aud = "https://otherexample.org" diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 86aede755..4363f3262 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -18,6 +18,7 @@ db-root-spec = "root" db-schemas = "test,tenant1,tenant2" db-config = true db-pre-config = "postgrest.preconf" +db-timezone-enabled = false db-tx-end = "commit-allow-override" db-uri = "postgresql://" jwt-aud = "https://example.org" diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index a131be048..315628773 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -18,6 +18,7 @@ db-root-spec = "openapi_v3" db-schemas = "multi,tenant,setup" db-config = false db-pre-config = "postgrest.pre_config" +db-timezone-enabled = false db-tx-end = "rollback-allow-override" db-uri = "tmp_db" jwt-aud = "https://postgrest.org" diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index 2d00d680d..fac4d596d 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/expected/utf-8.config b/test/io/configs/expected/utf-8.config index c06c50f3b..7f29a498c 100644 --- a/test/io/configs/expected/utf-8.config +++ b/test/io/configs/expected/utf-8.config @@ -18,6 +18,7 @@ db-root-spec = "" db-schemas = "public" db-config = true db-pre-config = "" +db-timezone-enabled = true db-tx-end = "commit" db-uri = "postgresql://" jwt-aud = "" diff --git a/test/io/configs/no-defaults-env.yaml b/test/io/configs/no-defaults-env.yaml index d9fea4c43..ce9280e1e 100644 --- a/test/io/configs/no-defaults-env.yaml +++ b/test/io/configs/no-defaults-env.yaml @@ -20,6 +20,7 @@ PGRST_DB_ROOT_SPEC: openapi_v3 PGRST_DB_SCHEMAS: multi, tenant,setup PGRST_DB_CONFIG: false PGRST_DB_PRE_CONFIG: "postgrest.pre_config" +PGRST_DB_TIMEZONE_ENABLED: false PGRST_DB_TX_END: rollback-allow-override PGRST_DB_URI: tmp_db PGRST_DB_USE_LEGACY_GUCS: false diff --git a/test/io/configs/no-defaults.config b/test/io/configs/no-defaults.config index f0e03e42e..6bb1cec15 100644 --- a/test/io/configs/no-defaults.config +++ b/test/io/configs/no-defaults.config @@ -18,6 +18,7 @@ db-root-spec = "openapi_v3" db-schemas = "multi, tenant,setup" db-config = false db-pre-config = "postgrest.pre_config" +db-timezone-enabled = false db-tx-end = "rollback-allow-override" db-uri = "tmp_db" jwt-aud = "https://postgrest.org" diff --git a/test/io/fixtures/db_config.sql b/test/io/fixtures/db_config.sql index bf7880f43..112271919 100644 --- a/test/io/fixtures/db_config.sql +++ b/test/io/fixtures/db_config.sql @@ -13,6 +13,7 @@ ALTER ROLE db_config_authenticator SET pgrst.db_pre_request = 'test.custom_heade ALTER ROLE db_config_authenticator SET pgrst.db_prepared_statements = 'false'; ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root'; ALTER ROLE db_config_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2'; +ALTER ROLE db_config_authenticator SET pgrst.db_timezone_enabled = 'false'; ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override'; ALTER ROLE db_config_authenticator SET pgrst.jwt_aud = 'https://example.org'; ALTER ROLE db_config_authenticator SET pgrst.jwt_cache_max_entries = '86400'; @@ -67,6 +68,7 @@ ALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_hea ALTER ROLE other_authenticator SET pgrst.db_prepared_statements = 'false'; ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root'; ALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2'; +ALTER ROLE other_authenticator SET pgrst.db_timezone_enabled = 'false'; ALTER ROLE other_authenticator SET pgrst.jwt_aud = 'https://otherexample.org'; ALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE'; ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'false'; diff --git a/test/observability/ObsHelper.hs b/test/observability/ObsHelper.hs index cabe8c42d..fb897390e 100644 --- a/test/observability/ObsHelper.hs +++ b/test/observability/ObsHelper.hs @@ -91,6 +91,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in , configDbSchemas = fromList ["test"] , configDbConfig = False , configDbPreConfig = Nothing + , configDbTimezoneEnabled = True , configDbUri = "postgresql://" , configFilePath = Nothing , configJWKS = rightToMaybe $ parseSecret secret diff --git a/test/spec/Feature/Query/Preferences/TimezoneSpec.hs b/test/spec/Feature/Query/Preferences/TimezoneSpec.hs index e0b89ca09..12603390a 100644 --- a/test/spec/Feature/Query/Preferences/TimezoneSpec.hs +++ b/test/spec/Feature/Query/Preferences/TimezoneSpec.hs @@ -10,9 +10,9 @@ import Test.Hspec.Wai.JSON import Protolude hiding (get) import SpecHelper -spec :: SpecWith ((), Application) -spec = - describe "test Prefer: timezone" $ do +enabledSpec :: SpecWith ((), Application) +enabledSpec = + 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" @@ -60,3 +60,34 @@ spec = { matchStatus = 200 , matchHeaders = [matchContentTypeJson , "Preference-Applied" <:> "handling=lenient"]} + + +disabledSpec :: SpecWith ((), Application) +disabledSpec = + describe "test Prefer: timezone with db-timezone-enabled is false" $ do + context "test Prefer: timezone=America/Los_Angeles when timezone is disabled" $ do + it "should throw error with handling=strict" $ + request methodGet "/timestamps" + [("Prefer", "handling=strict, timezone=America/Los_Angeles")] + "" + `shouldRespondWith` + [json|{"code":"PGRST122","details":"Invalid preferences: timezone=America/Los_Angeles","hint":null,"message":"Invalid preferences given with handling=strict"}|] + { matchStatus = 400 } + + it "should return with default timezone without handling or with handling=lenient" $ do + request methodGet "/timestamps" + [("Prefer", "timezone=America/Los_Angeles")] + "" + `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]} + + request methodGet "/timestamps" + [("Prefer", "handling=lenient, timezone=America/Los_Angeles")] + "" + `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"]} diff --git a/test/spec/Main.hs b/test/spec/Main.hs index 4e5a4b66e..a7f9ee7aa 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -128,6 +128,7 @@ main = do unicodeApp = appDbs testUnicodeCfg multipleSchemaApp = appDbs testMultipleSchemaCfg ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg + timezoneDisabled = appDbs testCfgTimezoneDisabled let analyze :: IO () @@ -157,7 +158,6 @@ main = do , ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.disabledSpec) , ("Feature.Query.Preferences.HandlingSpec" , Feature.Query.Preferences.HandlingSpec.spec) , ("Feature.Query.Preferences.MaxAffectedSpec" , Feature.Query.Preferences.MaxAffectedSpec.spec) - , ("Feature.Query.Preferences.TimezoneSpec" , Feature.Query.Preferences.TimezoneSpec.spec) , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) @@ -257,6 +257,15 @@ main = do parallel $ before withApp $ describe "Feature.Query.AggregateFunctionsDisallowedSpec." Feature.Query.AggregateFunctionsSpec.disallowed + -- this test runs with db-timezone-enabled = true + parallel $ before withApp $ + describe "Feature.Query.Preferences.TimezoneSpec.enabledSpec" Feature.Query.Preferences.TimezoneSpec.enabledSpec + + -- this test runs with db-timezone-enabled = false + parallel $ before timezoneDisabled $ + describe "Feature.Query.Preferences.TimezoneSpec.disabledSpec" Feature.Query.Preferences.TimezoneSpec.disabledSpec + + -- Note: the rollback tests can not run in parallel, because they test persistance and -- this results in race conditions diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index 17ae21c0b..6b50ec5a4 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -132,6 +132,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in , configDbSchemas = fromList ["test"] , configDbConfig = False , configDbPreConfig = Nothing + , configDbTimezoneEnabled = True , configDbUri = "postgresql://" , configFilePath = Nothing , configJWKS = rightToMaybe $ parseSecret secret @@ -254,6 +255,9 @@ testCfgServerTiming = baseCfg { configDbPlanEnabled = True } testCfgAggregatesEnabled :: AppConfig testCfgAggregatesEnabled = baseCfg { configDbAggregates = True } +testCfgTimezoneDisabled :: AppConfig +testCfgTimezoneDisabled = baseCfg { configDbTimezoneEnabled = False } + analyzeTable :: Text -> IO () analyzeTable tableName = void $ readProcess "psql" ["-U", "postgres", "--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []