-
Notifications
You must be signed in to change notification settings - Fork 108
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
Support UPDATE/DELETE .. RETURNING #362
base: master
Are you sure you want to change the base?
Support UPDATE/DELETE .. RETURNING #362
Conversation
One more thing... as I read around, Not sure what to make of that; my usecase is Postgres, so... 🤷 |
Gee, |
8461748
to
92373b1
Compare
Changelog rebased after v3.5.10.0 release. @parsonsmatt @ivanbakel please review |
Nicely wrapping I'd tried several times; the best I got is a failing test that generates the expected query:
but then result readout fails with
But then to use the other Factoring in the complete radio silence from maintainers — I won't invest much further effort. |
3ccc7c1
to
a9e27c5
Compare
Hello @parsonsmatt @9999years would you be so kind to take a look. Review the PR? Changelog rebased once again, and |
With bitemyapp#44 as a starting point, and more ideas in mind.
* TypeOperators -- enabled twice on lines 3, 23 * TypeApplications -- enabled twice on lines 4, 21 * DerivingStrategies -- enabled twice on lines 5, 9 * GeneralizedNewtypeDeriving -- enabled twice on lines 5, 15 * MultiParamTypeClasses -- implied by FunctionalDependencies This reduces lints from hlint & compiler warnings.
a9e27c5
to
564cb83
Compare
Hey, I'm sorry it's taken me a long time to look at this. I'll review it today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution! I apologize again for taking so long to review it.
I've suggested an alternative implementation which should be safer and possibly even slightly simpler. The backbone comes from refactoring toRawSql
to expose the ret
from the SqlQuery
, which allows you to append the RETURNING
clause directly and avoid modifying SideData
:
-- | (Internal) Pretty prints a 'SqlQuery' into a SQL query.
--
-- Note: if you're curious about the SQL query being generated by
-- @esqueleto@, instead of manually using this function (which is
-- possible but tedious), see the 'renderQueryToText' function (along with
-- 'renderQuerySelect', 'renderQueryUpdate', etc).
toRawSql
:: (SqlSelect a r, BackendCompatible SqlBackend backend)
=> Mode -> (backend, IdentState) -> SqlQuery a -> (TLB.Builder, [PersistValue])
toRawSql mode queryEnv =
snd . toRawSqlWith mode queryEnv
-- | Render a 'SqlQuery' into the internal representation of the returned
-- result, the text 'TLB.Builder', and the @['PersistValue']@ to interpolate
-- into the query text.
toRawSqlWith
:: (SqlSelect a r, BackendCompatible SqlBackend backend)
=> Mode -> (backend, IdentState) -> SqlQuery a -> (a, (TLB.Builder, [PersistValue]))
toRawSqlWith mode (conn, firstIdentState) query =
let ((ret, sd), finalIdentState) =
flip S.runState firstIdentState $
W.runWriterT $
unQ query
deleteRepeatedNewlines txt =
let
(preNewlines, rest) = TL.break (== '\n') txt
(_, rest') = TL.break (/= '\n') rest
in
if TL.null rest'
then preNewlines <> "\n"
else preNewlines <> "\n" <> deleteRepeatedNewlines rest'
SideData distinctClause
fromClauses
setClauses
whereClauses
groupByClause
havingClause
orderByClauses
limitClause
lockingClause
cteClause = sd
-- Pass the finalIdentState (containing all identifiers
-- that were used) to the subsequent calls. This ensures
-- that no name clashes will occur on subqueries that may
-- appear on the expressions below.
info = (projectBackend conn, finalIdentState)
overFst f (a, b) = (f a, b)
convert = TLB.fromLazyText . deleteRepeatedNewlines . TL.strip . TLB.toLazyText
in (,) ret $ overFst convert $ mconcat $ intersperse ("\n", [])
[ makeCte info cteClause
, makeInsertInto info mode ret
, makeSelect info mode distinctClause ret
, makeFrom info mode fromClauses
, makeSet info setClauses
, makeWhere info whereClauses
, makeGroupBy info groupByClause
, makeHaving info havingClause
, makeOrderBy info orderByClauses
, makeLimit info limitClause
, makeLocking info lockingClause
]
data ReturningClause | ||
= ReturningNothing -- ^ The default, absent clause. | ||
| ReturningStar -- ^ @RETURNING@ is present. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use Maybe ReturningClause
as the SideData
type, then this has one fewer case:
data ReturningClause | |
= ReturningNothing -- ^ The default, absent clause. | |
| ReturningStar -- ^ @RETURNING@ is present. | |
data ReturningClause | |
= ReturningStar -- ^ @RETURNING@ is present. |
But ReturningStar
itself isn't carring any useful information - we could further fold that Maybe ReturningClause
into just a Bool
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep you're right; I initially tried something different, this had more constructors — but it ended up isomorphic to Maybe ().
Will refactor.
makeReturning :: SqlSelect a r | ||
=> IdentInfo -> ReturningClause -> a -> (TLB.Builder, [PersistValue]) | ||
makeReturning _ ReturningNothing _ = mempty | ||
makeReturning info ReturningStar ret = ("RETURNING ", []) <> sqlSelectCols info ret |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is getting the SqlSelect a r
instance and using that to provide sqlSelectCols info ret
here.
A typical update
in esqueleto
has type update :: (SqlExpr (Entity val) -> SqlQuery ()) -> SqlPersistT m ()
. This would give us an a
of ()
, which would use this instance:
-- | Not useful for 'select', but used for 'update' and 'delete'.
instance SqlSelect () () where
sqlSelectCols _ _ = ("1", [])
sqlSelectColCount _ = 1
sqlSelectProcessRow _ = Right ()
RETURNING 1
.
If this clause ReturningStar
is enabled in a non-update/insert/delete - ie,
oops = select do
t <- from $ table @Foo
tellReturning ReturningStar
pure t
Then we'll generate a query like:
SELECT foo.a, foo.b, foo.c
FROM foo
RETURNING foo.a, foo.b, foo.c
Which is definitely an error.
So the possibility of error here makes me feel like exploring a different approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I thought about that, but tellReturning
is in Internal
module... its haddock says so, too:
-- | (Internal) Remember a @RETURNING@ clause in a query
tellReturning :: ReturningClause -> SqlQuery ()
The public interface is supposed to be only the added functions in Database.Esqueleto.PostgreSQL
(plus associated instances).
Further, the rest of query builder logic didn't seem like it provided any sql correctness guarantees internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah,it's not a generally safe thing, but I do want to avoid adding more unsafety where possible.
src/Database/Esqueleto/PostgreSQL.hs
Outdated
updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) | ||
=> (from -> SqlQuery ex) | ||
-> R.ReaderT backend m [ret] | ||
updateReturning block = do | ||
conn <- R.ask | ||
conduit <- rawSelectSource UPDATE (tellReturning ReturningStar >> from block) | ||
liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is the use of the new functionality - neat.
This is where we say "Do the RETURNING
thing":
rawSelectSource UPDATE (tellReturning ReturningStar >> from block)
So we modify the SideData
for the query, then it gets rendered and turned into a conduit
, which we run.
I think we can avoid modifying SideData
and still mostly keep this interface.
Consider renderQueryToText
. This takes a SqlQuery a
and renders it out.
If the SqlQuery a
has a ReturningStar
in the SideData
, then we graft on the appropriate text to the query. However, we can just do that directly without modifying the internals and introducing potential unsafety.
If we look at the code for rawSelectSource
, we can see that it's just doing toRawSql
on the query and then rawQueryRes
. The rest is handling the Conduit
(which imo isn't super useful since the query must be loaded into memory anyway).
So, if we inline this and use rawQuery
instead (which just does the list directly), we can do:
updateReturning block = do
conn <- R.ask
let
query' = toRawSql UPDATE (conn, initialIdentState) block
((ret, _sd), _finalIdent) = flip S.runState -- ... (from toRawSql)
returningClause = ("RETURNING ", []) <> sqlSelectCols info ret
(queryWithReturning, vals) =
query' <> ("\n", []) <> returningClause
rawQuery queryWithReturning vals
Now, it is unsatisfying that we have to run the query building twice with this - once in toRawSql
and once to get the ret
to make the returningClause
with.
If toRawSql
is refactored into a variant that also returns the ret
, then I think we're able to avoid that - then this can be:
conn <- R.ask
let
(ret, query') = toRawSqlWith UPDATE (conn, initialIdentState) block
returningClause = ("\nRETURNING ", []) <> sqlSelectCols info ret
(queryWithReturning, vals) =
query' <> returningClause
rawQuery queryWithReturning vals
Then I think there's probably virtue in writing makeReturning :: (InferReturning a r) => a -> (TLB.Builder, [PersistValue])
which can then be reused in deleteReturning
.
3.5.11.3 (unreleased) | ||
======== | ||
- @ulidtko | ||
- [#362](https://github.com/bitemyapp/esqueleto/pull/362) | ||
- Add `updateReturning`, `deleteReturning` as Postgres extensions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation adds a field to the SideData
type, which unfortunately does make this a breaking change and would need to go out as 3.6.0.0
.
However, I've suggested an alternative implementation that should be strictly additive, allowing it to be 3.5.12.0
.
Thanks for review @parsonsmatt 🙏 Easy nits done; I'll work on this some more. Your suggestion does make sense, I see its merits. One question I got which could use your input: how to implement the |
insertSelectReturning
:: (SqlSelect entity x, SqlSelect returning r) =>
=> SqlQuery (SqlExpr (Insertion entity), returning)
-> ReaderT backend m r So folks would write: f :: SqlPersistT m [FooId]
f = do
insertSelectReturning do
t <- from $ table @Foo
pure
( Bar <# (t ^. FooName) <&> (t ^. FooBar)
, t ^. FooId
) I think you can do It may also be better to define a |
So I just hit a reason to implement So my idea for would be something like data OnConflictDo ent
= DoNothing
| DoUpdate (SqlExpr (Entity ent) -> SqlExpr (Entity ent) -> [SqlExpr (Entity ent) ->SqlExpr Update])
runInsertReturning :: SqlSelect a r => SqlInsert backend insertType a -> ReaderT backend m [r]
onConflict :: EI.KnowResult uniq ~ Unique ent => uniq -> OnConflictDo ent -> SqlInsert backend ent a -> SqlInsert backend ent a
let doNothing = DoNothing
in insertFrom query
& onConflict UniqueField doNothing
& runInsertReturning |
Having worked with this a bit more, the backend can always just be SqlBackend inside of the SqlInsert type. Additionally runInsertReturning should take a projection function to determine the output rather than figuring out how to return an |
Howdy!
This implements #44 🎉 The aggregate issue #306 is also related.
beginner friendly
indeed 👌 was a bit scary at first, but I found a way! Pretty pleased with the result.Kindly review & merge 🙏
PR template checklist:
Bumped the version number.
Documented new APIs with Haddock markup.
Added
@since
declarations to the Haddock.Ran
⚠️ this fails with syntax error:
stylish-haskell
and otherwise adhered to the style guideBTW that weird
sub_select
deprecation notice is also breaking VS Code syntax highlighting, badly. Vim handles it, but also emits a parse error:Edit: fixed ✔️ in one of the PR commits.
After submitting your PR: