Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add config to disable Prefer: count=exact #2984

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2771, Add `Server-Timing` header with JWT duration - @taimoorzaeem
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
- #2777, Add config `db-exact-count-enable` to disable `Prefer: count=exact` - @taimoorzaeem

### Fixed

Expand Down
1 change: 1 addition & 0 deletions postgrest.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ test-suite spec
Feature.Query.AndOrParamsSpec
Feature.Query.ComputedRelsSpec
Feature.Query.DeleteSpec
Feature.Query.DisableExactCountSpec
Feature.Query.EmbedDisambiguationSpec
Feature.Query.EmbedInnerJoinSpec
Feature.Query.ErrorSpec
Expand Down
2 changes: 1 addition & 1 deletion src/PostgREST/ApiRequest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ userApiRequest conf req reqBody = do
, iRange = ranges
, iTopLevelRange = topLevelRange
, iPayload = payload
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf, configDbExactCountEnable conf) hdrs
, iQueryParams = qPrms
, iColumns = columns
, iHeaders = iHdrs
Expand Down
22 changes: 13 additions & 9 deletions src/PostgREST/ApiRequest/Preferences.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ data Preferences
--
-- One header with comma-separated values can be used to set multiple preferences:
--
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")]
-- >>> pPrint $ fromHeaders (True,True) [("Prefer", "resolution=ignore-duplicates, count=exact")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
Expand All @@ -71,7 +71,7 @@ data Preferences
--
-- Multiple headers can also be used:
--
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")]
-- >>> pPrint $ fromHeaders (True,True) [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
Expand All @@ -85,13 +85,13 @@ data Preferences
--
-- If a preference is set more than once, only the first is used:
--
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
-- >>> preferTransaction $ fromHeaders (True,True) [("Prefer", "tx=commit, tx=rollback")]
-- Just Commit
--
-- This is also the case across multiple headers:
--
-- >>> :{
-- preferResolution . fromHeaders True $
-- preferResolution . fromHeaders (True,True) $
-- [ ("Prefer", "resolution=ignore-duplicates")
-- , ("Prefer", "resolution=merge-duplicates")
-- ]
Expand All @@ -101,7 +101,7 @@ data Preferences
--
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
--
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- >>> pPrint $ fromHeaders (True,True) [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- Preferences
-- { preferResolution = Nothing
-- , preferRepresentation = Just Full
Expand All @@ -113,14 +113,18 @@ data Preferences
-- , invalidPrefs = [ "anything" ]
-- }
--
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
fromHeaders allowTxEndOverride headers =
fromHeaders :: (Bool,Bool) -> [HTTP.Header] -> Preferences
fromHeaders (dbAllowTxOverride,exactCountEnable) headers =
Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
, preferParameters = parsePrefs [SingleObject]
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
, preferCount = if exactCountEnable
then parsePrefs [ExactCount, PlannedCount, EstimatedCount]
else parsePrefs [PlannedCount, EstimatedCount]
, preferTransaction = if dbAllowTxOverride
then parsePrefs [Commit, Rollback]
else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient]
, invalidPrefs = filter (`notElem` acceptedPrefs) prefs
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ exampleConfigFile =
|## Function for in-database configuration
|## db-pre-config = "postgrest.pre_config"
|
|## Enable or Disable `Prefer: count=exact`
|# db-exact-count-enable = true
|
|## Extra schemas to add to the search_path of every request
|db-extra-search-path = "public"
|
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ data AppConfig = AppConfig
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
, configDbExactCountEnable :: Bool
, configDbExtraSearchPath :: [Text]
, configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool
Expand Down Expand Up @@ -142,6 +143,7 @@ toText conf =
[("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
,("db-channel", q . configDbChannel)
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
,("db-exact-count-enable", T.toLower . show . configDbExactCountEnable)
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
Expand Down Expand Up @@ -236,6 +238,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> (fromMaybe True <$> optBool "db-channel-enabled")
<*> (fromMaybe True <$> optBool "db-exact-count-enable")
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
<*> optWithAlias (optInt "db-max-rows")
(optInt "max-rows")
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/aliases.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-exact-count-enable = true
db-extra-search-path = "public"
db-max-rows = 1000
db-plan-enabled = false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-numeric.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-exact-count-enable = true
db-extra-search-path = "public"
db-max-rows = ""
db-plan-enabled = false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-string.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-exact-count-enable = true
db-extra-search-path = "public"
db-max-rows = ""
db-plan-enabled = false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/defaults.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-exact-count-enable = true
db-extra-search-path = "public"
db-max-rows = ""
db-plan-enabled = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = "pre_config_role"
db-channel = "postgrest"
db-channel-enabled = false
db-exact-count-enable = false
db-extra-search-path = "public,extensions,other"
db-max-rows = 100
db-plan-enabled = true
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults-with-db.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = "anonymous"
db-channel = "postgrest"
db-channel-enabled = false
db-exact-count-enable = false
db-extra-search-path = "public,extensions,private"
db-max-rows = 1000
db-plan-enabled = true
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = "root"
db-channel = "postgrest"
db-channel-enabled = false
db-exact-count-enable = false
db-extra-search-path = "public,test"
db-max-rows = 1000
db-plan-enabled = true
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/types.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-exact-count-enable = true
db-extra-search-path = "public"
db-max-rows = ""
db-plan-enabled = false
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PGRST_APP_SETTINGS_test: test
PGRST_DB_ANON_ROLE: root
PGRST_DB_CHANNEL: postgrest
PGRST_DB_CHANNEL_ENABLED: false
PGRST_DB_EXACT_COUNT_ENABLE: false
PGRST_DB_EXTRA_SEARCH_PATH: public, test
PGRST_DB_MAX_ROWS: 1000
PGRST_DB_PLAN_ENABLED: true
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
db-anon-role = "root"
db-channel = "postgrest"
db-channel-enabled = false
db-exact-count-enable = false
db-extra-search-path = "public, test"
db-max-rows = 1000
db-plan-enabled = true
Expand Down
27 changes: 27 additions & 0 deletions test/spec/Feature/Query/DisableExactCountSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Feature.Query.DisableExactCountSpec where

import Network.Wai (Application)

import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai

import Protolude hiding (get)
import SpecHelper

spec :: SpecWith ((), Application)
spec =
describe "tests for config db-exact-count-enable" $ do

context "should not return exact count" $ do
it "returns * in content-range instead of exact count" $ do
request methodHead "/getallprojects_view"
[("Prefer", "count=exact")]
""
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-4/*"]
}

6 changes: 6 additions & 0 deletions test/spec/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import qualified Feature.OptionsSpec
import qualified Feature.Query.AndOrParamsSpec
import qualified Feature.Query.ComputedRelsSpec
import qualified Feature.Query.DeleteSpec
import qualified Feature.Query.DisableExactCountSpec
import qualified Feature.Query.EmbedDisambiguationSpec
import qualified Feature.Query.EmbedInnerJoinSpec
import qualified Feature.Query.ErrorSpec
Expand Down Expand Up @@ -112,6 +113,7 @@ main = do
pgSafeUpdateApp = app testPgSafeUpdateEnabledCfg
obsApp = app testObservabilityCfg
serverTiming = app testCfgServerTiming
exactCountEnable = app testCfgExactCountEnable

extraSearchPathApp = appDbs testCfgExtraSearchPath
unicodeApp = appDbs testUnicodeCfg
Expand Down Expand Up @@ -252,6 +254,10 @@ main = do
parallel $ before serverTiming $
describe "Feature.Query.ServerTimingSpec.spec" Feature.Query.ServerTimingSpec.spec

-- this test runs with db-exact-count-enable = false
before exactCountEnable $
describe "Feature.Query.DisableExactCountSpec" Feature.Query.DisableExactCountSpec.spec

-- Note: the rollback tests can not run in parallel, because they test persistance and
-- this results in race conditions

Expand Down
4 changes: 4 additions & 0 deletions test/spec/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
, configDbExactCountEnable = True
, configDbExtraSearchPath = []
, configDbMaxRows = Nothing
, configDbPlanEnabled = False
Expand Down Expand Up @@ -236,6 +237,9 @@ testObservabilityCfg = baseCfg { configServerTraceHeader = Just $ mk "X-Request-
testCfgServerTiming :: AppConfig
testCfgServerTiming = baseCfg { configDbPlanEnabled = True }

testCfgExactCountEnable :: AppConfig
testCfgExactCountEnable = baseCfg { configDbExactCountEnable = False }

analyzeTable :: Text -> IO ()
analyzeTable tableName =
void $ readProcess "psql" ["-U", "postgres", "--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
Expand Down
Loading