diff --git a/postgrest.cabal b/postgrest.cabal index ad6a0a18d..afe56c658 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -242,7 +242,9 @@ test-suite spec Feature.Query.PgSafeUpdateSpec Feature.Query.PlanSpec Feature.Query.PostGISSpec - Feature.Query.PreferencesSpec + Feature.Query.Preferences.HandlingSpec + Feature.Query.Preferences.MaxAffectedSpec + Feature.Query.Preferences.TimezoneSpec Feature.Query.QueryLimitedSpec Feature.Query.QuerySpec Feature.Query.RangeSpec diff --git a/test/spec/Feature/Query/Preferences/HandlingSpec.hs b/test/spec/Feature/Query/Preferences/HandlingSpec.hs new file mode 100644 index 000000000..23c095a14 --- /dev/null +++ b/test/spec/Feature/Query/Preferences/HandlingSpec.hs @@ -0,0 +1,72 @@ +module Feature.Query.Preferences.HandlingSpec where + +import Network.Wai (Application) + +import Network.HTTP.Types +import Test.Hspec +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON + +import Protolude hiding (get) +import SpecHelper + +spec :: SpecWith ((), Application) +spec = + describe "test Prefer: handling" $ do + context "check behaviour of Prefer: handling=strict" $ do + it "throws error when handling=strict and invalid prefs are given" $ + request methodGet "/items" [("Prefer", "handling=strict, anything")] "" + `shouldRespondWith` + [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] + { matchStatus = 400 } + + it "throw error when handling=strict and invalid prefs are given with multiples in separate prefers" $ + request methodGet "/items" [("Prefer", "handling=strict"),("Prefer","something, else")] "" + `shouldRespondWith` + [json|{"details":"Invalid preferences: something, else","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] + { matchStatus = 400 } + + it "throws error with post request" $ + request methodPost "/organizations?select=*" + [("Prefer","return=representation, handling=strict, anything")] + [json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|] + `shouldRespondWith` + [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] + { matchStatus = 400 } + + it "throws error with rpc" $ + request methodPost "/rpc/overloaded_unnamed_param" + [("Content-Type", "application/json"), ("Prefer", "handling=strict, anything")] + [json|{}|] + `shouldRespondWith` + [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] + { matchStatus = 400 } + + context "check behaviour of Prefer: handling=lenient" $ do + it "does not throw error when handling=lenient and invalid prefs" $ + request methodGet "/items" [("Prefer", "handling=lenient, anything")] "" + `shouldRespondWith` 200 + + it "does not throw error when handling=lenient and invalid prefs in multiples prefers" $ + request methodGet "/items" [("Prefer", "handling=lenient"), ("Prefer", "anything")] "" + `shouldRespondWith` 200 + + it "does not throw error with post request" $ + request methodPost "/organizations?select=*" + [("Prefer","return=representation, handling=lenient, anything")] + [json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|] + `shouldRespondWith` + [json|[{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}]|] + { matchStatus = 201 + , matchHeaders = [ matchContentTypeJson ] + } + + it "does not throw error with rpc" $ + request methodPost "/rpc/overloaded_unnamed_param" + [("Content-Type", "application/json"), ("Prefer", "handling=lenient, anything")] + [json|{}|] + `shouldRespondWith` + [json| 1 |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + } diff --git a/test/spec/Feature/Query/Preferences/MaxAffectedSpec.hs b/test/spec/Feature/Query/Preferences/MaxAffectedSpec.hs new file mode 100644 index 000000000..634fd805d --- /dev/null +++ b/test/spec/Feature/Query/Preferences/MaxAffectedSpec.hs @@ -0,0 +1,130 @@ +module Feature.Query.Preferences.MaxAffectedSpec where + +import Network.Wai (Application) + +import Network.HTTP.Types +import Test.Hspec +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON + +import Protolude hiding (get) + +spec :: SpecWith ((), Application) +spec = + describe "test Prefer: max-affected" $ do + context "test Prefer: max-affected with handling=strict" $ do + it "should fail if items deleted more than 10" $ + request methodDelete "/items?id=lt.15" + [("Prefer", "handling=strict, max-affected=10")] + "" + `shouldRespondWith` + [json|{"code":"PGRST124","details":"The query affects 14 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|] + { matchStatus = 400 } + + it "should succeed if items deleted less than 10" $ + request methodDelete "/items?id=lt.10" + [("Prefer", "handling=strict, max-affected=10")] + "" + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=10"]} + + it "should fail if items updated more than 0" $ + request methodPatch "/tiobe_pls?name=eq.Java" + [("Prefer", "handling=strict, max-affected=0")] + [json| [{"name":"Java", "rank":19}] |] + `shouldRespondWith` + [json|{"code":"PGRST124","details":"The query affects 1 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|] + { matchStatus = 400 } + + it "should succeed if items updated equal 1" $ + request methodDelete "/tiobe_pls?name=eq.Java" + [("Prefer", "handling=strict, max-affected=1")] + [json| [{"name":"Java", "rank":19}] |] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=1"]} + + context "test Prefer: max-affected with handling=lenient" $ do + it "should not fail" $ + request methodDelete "/items?id=lt.15" + [("Prefer", "handling=lenient, max-affected=10")] + "" + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} + + it "should succeed if items deleted less than 10" $ + request methodDelete "/items?id=lt.10" + [("Prefer", "handling=lenient, max-affected=10")] + "" + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} + + it "should not fail" $ + request methodPatch "/tiobe_pls?name=eq.Java" + [("Prefer", "handling=lenient, max-affected=0")] + [json| [{"name":"Java", "rank":19}] |] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} + + it "should succeed if items updated equal 1" $ + request methodDelete "/tiobe_pls?name=eq.Java" + [("Prefer", "handling=lenient, max-affected=1")] + [json| [{"name":"Java", "rank":19}] |] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} + + context "test Prefer: max-affected with rpc" $ do + it "should fail with rpc when deleting rows more than prefered with returns setof" $ + request methodPost "/rpc/delete_items_returns_setof" + [("Prefer", "handling=strict, max-affected=10")] + "" + `shouldRespondWith` + [json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |] + { matchStatus = 400 } + + it "should fail with rpc when deleting rows more than prefered with returns table" $ + request methodPost "/rpc/delete_items_returns_table" + [("Prefer", "handling=strict, max-affected=10")] + "" + `shouldRespondWith` + [json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |] + { matchStatus = 400 } + + it "should succeed with rpc deleting rows less than prefered with returns setof" $ + request methodPost "/rpc/delete_items_returns_setof" + [("Prefer", "handling=strict, max-affected=20")] + "" + `shouldRespondWith` + [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7}, + {"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}, + {"id":14},{"id":15}]|] + { matchStatus = 200 } + + it "should succeed with rpc deleting rows less than prefered with returns table" $ + request methodPost "/rpc/delete_items_returns_table" + [("Prefer", "handling=strict, max-affected=20")] + "" + `shouldRespondWith` + [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7}, + {"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}, + {"id":14},{"id":15}]|] + { matchStatus = 200 } + + it "should fail with rpc when returns void with handling=strict" $ + request methodPost "/rpc/delete_items_returns_void" + [("Prefer", "handling=strict, max-affected=20")] + "" + `shouldRespondWith` + [json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |] + { matchStatus = 400 } diff --git a/test/spec/Feature/Query/Preferences/TimezoneSpec.hs b/test/spec/Feature/Query/Preferences/TimezoneSpec.hs new file mode 100644 index 000000000..e0b89ca09 --- /dev/null +++ b/test/spec/Feature/Query/Preferences/TimezoneSpec.hs @@ -0,0 +1,62 @@ +module Feature.Query.Preferences.TimezoneSpec where + +import Network.Wai (Application) + +import Network.HTTP.Types +import Test.Hspec +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON + +import Protolude hiding (get) +import SpecHelper + +spec :: SpecWith ((), Application) +spec = + describe "test Prefer: timezone" $ 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"] } + + 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 } + + 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]} + + 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"]} diff --git a/test/spec/Feature/Query/PreferencesSpec.hs b/test/spec/Feature/Query/PreferencesSpec.hs deleted file mode 100644 index 4f2f7b8be..000000000 --- a/test/spec/Feature/Query/PreferencesSpec.hs +++ /dev/null @@ -1,238 +0,0 @@ -module Feature.Query.PreferencesSpec where - -import Network.Wai (Application) - -import Network.HTTP.Types -import Test.Hspec -import Test.Hspec.Wai -import Test.Hspec.Wai.JSON - -import Protolude hiding (get) -import SpecHelper - -spec :: SpecWith ((), Application) -spec = - describe "test prefer headers and preference-applied headers" $ do - - context "check behaviour of Prefer: handling=strict" $ do - it "throws error when handling=strict and invalid prefs are given" $ - request methodGet "/items" [("Prefer", "handling=strict, anything")] "" - `shouldRespondWith` - [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] - { matchStatus = 400 } - - it "throw error when handling=strict and invalid prefs are given with multiples in separate prefers" $ - request methodGet "/items" [("Prefer", "handling=strict"),("Prefer","something, else")] "" - `shouldRespondWith` - [json|{"details":"Invalid preferences: something, else","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] - { matchStatus = 400 } - - it "throws error with post request" $ - request methodPost "/organizations?select=*" - [("Prefer","return=representation, handling=strict, anything")] - [json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|] - `shouldRespondWith` - [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] - { matchStatus = 400 } - - it "throws error with rpc" $ - request methodPost "/rpc/overloaded_unnamed_param" - [("Content-Type", "application/json"), ("Prefer", "handling=strict, anything")] - [json|{}|] - `shouldRespondWith` - [json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|] - { matchStatus = 400 } - - context "check behaviour of Prefer: handling=lenient" $ do - it "does not throw error when handling=lenient and invalid prefs" $ - request methodGet "/items" [("Prefer", "handling=lenient, anything")] "" - `shouldRespondWith` 200 - - it "does not throw error when handling=lenient and invalid prefs in multiples prefers" $ - request methodGet "/items" [("Prefer", "handling=lenient"), ("Prefer", "anything")] "" - `shouldRespondWith` 200 - - it "does not throw error with post request" $ - request methodPost "/organizations?select=*" - [("Prefer","return=representation, handling=lenient, anything")] - [json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|] - `shouldRespondWith` - [json|[{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}]|] - { matchStatus = 201 - , matchHeaders = [ matchContentTypeJson ] - } - - it "does not throw error with rpc" $ - request methodPost "/rpc/overloaded_unnamed_param" - [("Content-Type", "application/json"), ("Prefer", "handling=lenient, anything")] - [json|{}|] - `shouldRespondWith` - [json| 1 |] - { matchStatus = 200 - , matchHeaders = [matchContentTypeJson] - } - - 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"] } - - 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 } - - 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]} - - 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"]} - - context "test Prefer: max-affected with handling=strict" $ do - it "should fail if items deleted more than 10" $ - request methodDelete "/items?id=lt.15" - [("Prefer", "handling=strict, max-affected=10")] - "" - `shouldRespondWith` - [json|{"code":"PGRST124","details":"The query affects 14 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|] - { matchStatus = 400 } - - it "should succeed if items deleted less than 10" $ - request methodDelete "/items?id=lt.10" - [("Prefer", "handling=strict, max-affected=10")] - "" - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=10"]} - - it "should fail if items updated more than 0" $ - request methodPatch "/tiobe_pls?name=eq.Java" - [("Prefer", "handling=strict, max-affected=0")] - [json| [{"name":"Java", "rank":19}] |] - `shouldRespondWith` - [json|{"code":"PGRST124","details":"The query affects 1 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"}|] - { matchStatus = 400 } - - it "should succeed if items updated equal 1" $ - request methodDelete "/tiobe_pls?name=eq.Java" - [("Prefer", "handling=strict, max-affected=1")] - [json| [{"name":"Java", "rank":19}] |] - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=strict, max-affected=1"]} - - context "test Prefer: max-affected with handling=lenient" $ do - it "should not fail" $ - request methodDelete "/items?id=lt.15" - [("Prefer", "handling=lenient, max-affected=10")] - "" - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} - - it "should succeed if items deleted less than 10" $ - request methodDelete "/items?id=lt.10" - [("Prefer", "handling=lenient, max-affected=10")] - "" - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} - - it "should not fail" $ - request methodPatch "/tiobe_pls?name=eq.Java" - [("Prefer", "handling=lenient, max-affected=0")] - [json| [{"name":"Java", "rank":19}] |] - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} - - it "should succeed if items updated equal 1" $ - request methodDelete "/tiobe_pls?name=eq.Java" - [("Prefer", "handling=lenient, max-affected=1")] - [json| [{"name":"Java", "rank":19}] |] - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = ["Preference-Applied" <:> "handling=lenient"]} - - context "test Prefer: max-affected with rpc" $ do - it "should fail with rpc when deleting rows more than prefered with returns setof" $ - request methodPost "/rpc/delete_items_returns_setof" - [("Prefer", "handling=strict, max-affected=10")] - "" - `shouldRespondWith` - [json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |] - { matchStatus = 400 } - - it "should fail with rpc when deleting rows more than prefered with returns table" $ - request methodPost "/rpc/delete_items_returns_table" - [("Prefer", "handling=strict, max-affected=10")] - "" - `shouldRespondWith` - [json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |] - { matchStatus = 400 } - - it "should succeed with rpc deleting rows less than prefered with returns setof" $ - request methodPost "/rpc/delete_items_returns_setof" - [("Prefer", "handling=strict, max-affected=20")] - "" - `shouldRespondWith` - [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7}, - {"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}, - {"id":14},{"id":15}]|] - { matchStatus = 200 } - - it "should succeed with rpc deleting rows less than prefered with returns table" $ - request methodPost "/rpc/delete_items_returns_table" - [("Prefer", "handling=strict, max-affected=20")] - "" - `shouldRespondWith` - [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7}, - {"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}, - {"id":14},{"id":15}]|] - { matchStatus = 200 } - - it "should fail with rpc when returns void with handling=strict" $ - request methodPost "/rpc/delete_items_returns_void" - [("Prefer", "handling=strict, max-affected=20")] - "" - `shouldRespondWith` - [json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |] - { matchStatus = 400 } diff --git a/test/spec/Main.hs b/test/spec/Main.hs index 400eea329..4e5a4b66e 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -52,7 +52,9 @@ import qualified Feature.Query.NullsStripSpec import qualified Feature.Query.PgSafeUpdateSpec import qualified Feature.Query.PlanSpec import qualified Feature.Query.PostGISSpec -import qualified Feature.Query.PreferencesSpec +import qualified Feature.Query.Preferences.HandlingSpec +import qualified Feature.Query.Preferences.MaxAffectedSpec +import qualified Feature.Query.Preferences.TimezoneSpec import qualified Feature.Query.QueryLimitedSpec import qualified Feature.Query.QuerySpec import qualified Feature.Query.RangeSpec @@ -153,7 +155,9 @@ main = do , ("Feature.Query.PgErrorCodeMappingSpec" , Feature.Query.ErrorSpec.pgErrorCodeMapping) , ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec) , ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.disabledSpec) - , ("Feature.Query.PreferencesSpec" , Feature.Query.PreferencesSpec.spec) + , ("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)