test(spec): move preference tests into separate modules

Towards #4751.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
This commit is contained in:
Taimoor Zaeem
2026-03-26 12:36:47 -05:00
committed by Steve Chavez
parent 135b77d12b
commit 1b402d16ad
6 changed files with 273 additions and 241 deletions
+3 -1
View File
@@ -242,7 +242,9 @@ test-suite spec
Feature.Query.PgSafeUpdateSpec Feature.Query.PgSafeUpdateSpec
Feature.Query.PlanSpec Feature.Query.PlanSpec
Feature.Query.PostGISSpec Feature.Query.PostGISSpec
Feature.Query.PreferencesSpec Feature.Query.Preferences.HandlingSpec
Feature.Query.Preferences.MaxAffectedSpec
Feature.Query.Preferences.TimezoneSpec
Feature.Query.QueryLimitedSpec Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec Feature.Query.QuerySpec
Feature.Query.RangeSpec Feature.Query.RangeSpec
@@ -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]
}
@@ -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 }
@@ -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"]}
-238
View File
@@ -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 }
+6 -2
View File
@@ -52,7 +52,9 @@ import qualified Feature.Query.NullsStripSpec
import qualified Feature.Query.PgSafeUpdateSpec import qualified Feature.Query.PgSafeUpdateSpec
import qualified Feature.Query.PlanSpec import qualified Feature.Query.PlanSpec
import qualified Feature.Query.PostGISSpec 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.QueryLimitedSpec
import qualified Feature.Query.QuerySpec import qualified Feature.Query.QuerySpec
import qualified Feature.Query.RangeSpec import qualified Feature.Query.RangeSpec
@@ -153,7 +155,9 @@ main = do
, ("Feature.Query.PgErrorCodeMappingSpec" , Feature.Query.ErrorSpec.pgErrorCodeMapping) , ("Feature.Query.PgErrorCodeMappingSpec" , Feature.Query.ErrorSpec.pgErrorCodeMapping)
, ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec) , ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec)
, ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.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.QuerySpec" , Feature.Query.QuerySpec.spec)
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)