Skip to content

Commit dbf99c6

Browse files
wolfgangwalthersteve-chavez
authored andcommitted
Add support for Prefer tx=rollback
1 parent 698fac8 commit dbf99c6

File tree

9 files changed

+290
-16
lines changed

9 files changed

+290
-16
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1515
- #1559, No downtime when reloading the schema cache with SIGUSR1 - @steve-chavez
1616
- #504, Add `log-level` config option. The admitted levels are: crit, error, warn and info - @steve-chavez
1717
- #1607, Enable embedding through multiple views recursively - @wolfgangwalther
18+
- #1598, Allow rollback of the transaction with Prefer tx=rollback - @wolfgangwalther
1819

1920
### Fixed
2021

postgrest.cabal

+4-3
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ test-suite spec
154154
Feature.DeleteSpec
155155
Feature.EmbedDisambiguationSpec
156156
Feature.ExtraSearchPathSpec
157+
Feature.HtmlRawOutputSpec
157158
Feature.InsertSpec
158159
Feature.JsonOperatorSpec
160+
Feature.MultipleSchemaSpec
159161
Feature.NoJwtSpec
160162
Feature.NonexistentSchemaSpec
161163
Feature.OpenApiSpec
@@ -164,16 +166,15 @@ test-suite spec
164166
Feature.QueryLimitedSpec
165167
Feature.QuerySpec
166168
Feature.RangeSpec
169+
Feature.RawOutputTypesSpec
170+
Feature.RollbackSpec
167171
Feature.RootSpec
168172
Feature.RpcPreRequestGucsSpec
169173
Feature.RpcSpec
170174
Feature.SingularSpec
171175
Feature.UnicodeSpec
172176
Feature.UpdateSpec
173177
Feature.UpsertSpec
174-
Feature.RawOutputTypesSpec
175-
Feature.HtmlRawOutputSpec
176-
Feature.MultipleSchemaSpec
177178
SpecHelper
178179
TestTypes
179180
hs-source-dirs: test

src/PostgREST/ApiRequest.hs

+14-10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ data ApiRequest = ApiRequest {
114114
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
115115
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
116116
, iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
117+
, iPreferTransaction :: Maybe PreferTransaction -- ^ Whether the clients wants to commit or rollback the transaction
117118
, iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
118119
, iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
119120
, iSelect :: Maybe Text -- ^ &select parameter used to shape the response
@@ -146,16 +147,19 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
146147
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
147148
, iPayload = relevantPayload
148149
, iPreferRepresentation = representation
149-
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
150-
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
151-
| otherwise -> Nothing
152-
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
153-
| hasPrefer (show PlannedCount) -> Just PlannedCount
154-
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
155-
| otherwise -> Nothing
156-
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
157-
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
158-
| otherwise -> Nothing
150+
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
151+
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
152+
| otherwise -> Nothing
153+
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
154+
| hasPrefer (show PlannedCount) -> Just PlannedCount
155+
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
156+
| otherwise -> Nothing
157+
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
158+
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
159+
| otherwise -> Nothing
160+
, iPreferTransaction = if | hasPrefer (show Commit) -> Just Commit
161+
| hasPrefer (show Rollback) -> Just Rollback
162+
| otherwise -> Nothing
159163
, iFilters = filters
160164
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
161165
, iSelect = toS <$> join (lookup "select" qParams)

src/PostgREST/App.hs

+9-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,15 @@ postgrest logLev refConf refDbStructure pool getTime connWorker =
8686
Right claims -> do
8787
let
8888
authed = containsRole claims
89-
handleReq = runPgLocals conf claims (app dbStructure conf) apiRequest
89+
shouldCommit = configTxAllowOverride conf && iPreferTransaction apiRequest == Just Commit
90+
shouldRollback = configTxAllowOverride conf && iPreferTransaction apiRequest == Just Rollback
91+
preferenceApplied
92+
| shouldCommit = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Commit))]
93+
| shouldRollback = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Rollback))]
94+
| otherwise = identity
95+
handleReq = do
96+
when (shouldRollback || (configTxRollbackAll conf && not shouldCommit)) HT.condemn
97+
mapResponseHeaders preferenceApplied <$> runPgLocals conf claims (app dbStructure conf) apiRequest
9098
dbResp <- P.use pool $ HT.transaction HT.ReadCommitted (txMode apiRequest) handleReq
9199
return $ either (errorResponseFor . PgError authed) identity dbResp
92100
-- Launch the connWorker when the connection is down. The postgrest function can respond successfully(with a stale schema cache) before the connWorker is done.

src/PostgREST/Config.hs

+16-2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ data AppConfig = AppConfig {
9797
, configJWKS :: Maybe JWKSet
9898

9999
, configLogLevel :: LogLevel
100+
101+
, configTxRollbackAll :: Bool
102+
, configTxAllowOverride :: Bool
100103
}
101104

102105
configPoolTimeout' :: (Fractional a) => AppConfig -> a
@@ -196,6 +199,15 @@ readPathShowHelp = customExecParser parserPrefs opts
196199
|
197200
|## logging level, the admitted values are: crit, error, warn and info.
198201
|# log-level = "error"
202+
|
203+
|## rollback all transactions by default, use for test environments
204+
|## disabled by default
205+
|# tx-rollback-all = false
206+
|
207+
|## allow overriding the tx-rollback-all setting for a request by
208+
|## setting the Prefer: tx=[commit|rollback] header
209+
|## disabled by default
210+
|# tx-allow-override = false
199211
|]
200212

201213
-- | Parse the config file
@@ -225,9 +237,9 @@ readAppConfig cfgPath = do
225237
<*> (fmap unpack <$> optString "server-unix-socket")
226238
<*> parseSocketFileMode "server-unix-socket-mode"
227239
<*> (fromMaybe "pgrst" <$> optString "db-channel")
228-
<*> ((Just True ==) <$> optBool "db-channel-enabled")
240+
<*> (fromMaybe False <$> optBool "db-channel-enabled")
229241
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
230-
<*> ((Just True ==) <$> optBool "secret-is-base64")
242+
<*> (fromMaybe False <$> optBool "secret-is-base64")
231243
<*> parseJwtAudience "jwt-aud"
232244
<*> (fromMaybe 10 <$> optInt "db-pool")
233245
<*> (fromMaybe 10 <$> optInt "db-pool-timeout")
@@ -240,6 +252,8 @@ readAppConfig cfgPath = do
240252
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
241253
<*> pure Nothing
242254
<*> parseLogLevel "log-level"
255+
<*> (fromMaybe False <$> optBool "tx-rollback-all")
256+
<*> (fromMaybe False <$> optBool "tx-allow-override")
243257

244258
parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode)
245259
parseSocketFileMode k =

src/PostgREST/Types.hs

+9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ instance Show PreferCount where
109109
show PlannedCount = "count=planned"
110110
show EstimatedCount = "count=estimated"
111111

112+
data PreferTransaction
113+
= Commit -- Commit transaction - the default.
114+
| Rollback -- Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
115+
deriving Eq
116+
117+
instance Show PreferTransaction where
118+
show Commit = "tx=commit"
119+
show Rollback = "tx=rollback"
120+
112121
data DbStructure = DbStructure {
113122
dbTables :: [Table]
114123
, dbColumns :: [Column]

test/Feature/RollbackSpec.hs

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
module Feature.RollbackSpec where
2+
3+
import Network.Wai (Application)
4+
5+
import Network.HTTP.Types
6+
import Test.Hspec
7+
import Test.Hspec.Wai
8+
import Test.Hspec.Wai.JSON
9+
10+
import Protolude hiding (get)
11+
import SpecHelper
12+
13+
-- two helpers functions to make sure that each test can setup and cleanup properly
14+
15+
-- creates Item to work with for PATCH and DELETE
16+
postItem =
17+
request methodPost "/items"
18+
[("Prefer", "resolution=ignore-duplicates")]
19+
[json|{"id":0}|]
20+
`shouldRespondWith`
21+
""
22+
{ matchStatus = 201 }
23+
24+
-- removes Items left over from POST, PUT, and PATCH
25+
deleteItems =
26+
delete "/items?id=lte.0"
27+
`shouldRespondWith`
28+
""
29+
{ matchStatus = 204 }
30+
31+
preferDefault = [("Prefer", "return=representation")]
32+
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
33+
preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]
34+
35+
withoutPreferenceApplied = []
36+
withPreferenceCommitApplied = [ "Preference-Applied" <:> "tx=commit" ]
37+
withPreferenceRollbackApplied = [ "Preference-Applied" <:> "tx=rollback" ]
38+
39+
shouldRespondToReads reqHeaders respHeaders = do
40+
it "responds to GET" $ do
41+
request methodGet "/items?id=eq.1"
42+
reqHeaders
43+
""
44+
`shouldRespondWith`
45+
[json|[{"id":1}]|]
46+
{ matchHeaders = respHeaders }
47+
48+
it "responds to HEAD" $ do
49+
request methodHead "/items?id=eq.1"
50+
reqHeaders
51+
""
52+
`shouldRespondWith`
53+
""
54+
{ matchHeaders = respHeaders }
55+
56+
it "responds to GET on RPC" $ do
57+
request methodGet "/rpc/search?id=1"
58+
reqHeaders
59+
""
60+
`shouldRespondWith`
61+
[json|[{"id":1}]|]
62+
{ matchHeaders = respHeaders }
63+
64+
it "responds to POST on RPC" $ do
65+
request methodPost "/rpc/search"
66+
reqHeaders
67+
[json|{"id":1}|]
68+
`shouldRespondWith`
69+
[json|[{"id":1}]|]
70+
{ matchHeaders = respHeaders }
71+
72+
shouldPersistMutations reqHeaders respHeaders = do
73+
it "does persist post" $ do
74+
request methodPost "/items"
75+
reqHeaders
76+
[json|{"id":0}|]
77+
`shouldRespondWith`
78+
[json|[{"id":0}]|]
79+
{ matchStatus = 201
80+
, matchHeaders = respHeaders }
81+
get "items?id=eq.0"
82+
`shouldRespondWith`
83+
[json|[{"id":0}]|]
84+
deleteItems
85+
86+
it "does persist put" $ do
87+
request methodPut "/items?id=eq.0"
88+
reqHeaders
89+
[json|{"id":0}|]
90+
`shouldRespondWith`
91+
[json|[{"id":0}]|]
92+
{ matchHeaders = respHeaders }
93+
get "items?id=eq.0"
94+
`shouldRespondWith`
95+
[json|[{"id":0}]|]
96+
deleteItems
97+
98+
it "does persist patch" $ do
99+
postItem
100+
request methodPatch "/items?id=eq.0"
101+
reqHeaders
102+
[json|{"id":-1}|]
103+
`shouldRespondWith`
104+
[json|[{"id":-1}]|]
105+
{ matchHeaders = respHeaders }
106+
get "items?id=eq.0"
107+
`shouldRespondWith`
108+
[json|[]|]
109+
get "items?id=eq.-1"
110+
`shouldRespondWith`
111+
[json|[{"id":-1}]|]
112+
deleteItems
113+
114+
it "does persist delete" $ do
115+
postItem
116+
request methodDelete "/items?id=eq.0"
117+
reqHeaders
118+
""
119+
`shouldRespondWith`
120+
[json|[{"id":0}]|]
121+
{ matchHeaders = respHeaders }
122+
get "items?id=eq.0"
123+
`shouldRespondWith`
124+
[json|[]|]
125+
126+
shouldNotPersistMutations reqHeaders respHeaders = do
127+
it "does not persist post" $ do
128+
request methodPost "/items"
129+
reqHeaders
130+
[json|{"id":0}|]
131+
`shouldRespondWith`
132+
[json|[{"id":0}]|]
133+
{ matchStatus = 201
134+
, matchHeaders = respHeaders }
135+
get "items?id=eq.0"
136+
`shouldRespondWith`
137+
[json|[]|]
138+
139+
it "does not persist put" $ do
140+
request methodPut "/items?id=eq.0"
141+
reqHeaders
142+
[json|{"id":0}|]
143+
`shouldRespondWith`
144+
[json|[{"id":0}]|]
145+
{ matchHeaders = respHeaders }
146+
get "items?id=eq.0"
147+
`shouldRespondWith`
148+
[json|[]|]
149+
150+
it "does not persist patch" $ do
151+
request methodPatch "/items?id=eq.1"
152+
reqHeaders
153+
[json|{"id":0}|]
154+
`shouldRespondWith`
155+
[json|[{"id":0}]|]
156+
{ matchHeaders = respHeaders }
157+
get "items?id=eq.0"
158+
`shouldRespondWith`
159+
[json|[]|]
160+
get "items?id=eq.1"
161+
`shouldRespondWith`
162+
[json|[{"id":1}]|]
163+
164+
it "does not persist delete" $ do
165+
request methodDelete "/items?id=eq.1"
166+
reqHeaders
167+
""
168+
`shouldRespondWith`
169+
[json|[{"id":1}]|]
170+
{ matchHeaders = respHeaders }
171+
get "items?id=eq.1"
172+
`shouldRespondWith`
173+
[json|[{"id":1}]|]
174+
175+
allowed :: SpecWith ((), Application)
176+
allowed = describe "tx-allow-override = true" $ do
177+
describe "without Prefer tx" $ do
178+
-- TODO: Change this to default to rollback for whole test-suite
179+
preferDefault `shouldRespondToReads` withoutPreferenceApplied
180+
preferDefault `shouldPersistMutations` withoutPreferenceApplied
181+
182+
describe "Prefer tx=commit" $ do
183+
preferCommit `shouldRespondToReads` withPreferenceCommitApplied
184+
preferCommit `shouldPersistMutations` withPreferenceCommitApplied
185+
186+
describe "Prefer tx=rollback" $ do
187+
preferRollback `shouldRespondToReads` withPreferenceRollbackApplied
188+
preferRollback `shouldNotPersistMutations` withPreferenceRollbackApplied
189+
190+
disallowed :: SpecWith ((), Application)
191+
disallowed = describe "tx-rollback-all = false, tx-allow-override = false" $ do
192+
describe "without Prefer tx" $ do
193+
preferDefault `shouldRespondToReads` withoutPreferenceApplied
194+
preferDefault `shouldPersistMutations` withoutPreferenceApplied
195+
196+
describe "Prefer tx=commit" $ do
197+
preferCommit `shouldRespondToReads` withoutPreferenceApplied
198+
preferCommit `shouldPersistMutations` withoutPreferenceApplied
199+
200+
describe "Prefer tx=rollback" $ do
201+
preferRollback `shouldRespondToReads` withoutPreferenceApplied
202+
preferRollback `shouldPersistMutations` withoutPreferenceApplied
203+
204+
205+
forced :: SpecWith ((), Application)
206+
forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do
207+
describe "without Prefer tx" $ do
208+
preferDefault `shouldRespondToReads` withoutPreferenceApplied
209+
preferDefault `shouldNotPersistMutations` withoutPreferenceApplied
210+
211+
describe "Prefer tx=commit" $ do
212+
preferCommit `shouldRespondToReads` withoutPreferenceApplied
213+
preferCommit `shouldNotPersistMutations` withoutPreferenceApplied
214+
215+
describe "Prefer tx=rollback" $ do
216+
preferRollback `shouldRespondToReads` withoutPreferenceApplied
217+
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied
218+

0 commit comments

Comments
 (0)