Skip to content

Commit c0248d2

Browse files
authored
Fix runtime SQL error for selectWith without CTEs (#814)
1 parent be7f9eb commit c0248d2

7 files changed

Lines changed: 67 additions & 16 deletions

File tree

beam-core/ChangeLog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.11.2.0
2+
3+
## Bug fixes
4+
5+
* Fixed an issue where using `selectWith` and no common-table expressions would lead to
6+
invalid SQL at runtime.
7+
18
# 0.11.1.0
29

310
## New features

beam-core/Database/Beam/Query.hs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ import Control.Monad.Identity
121121
import Control.Monad.Writer
122122
import Control.Monad.State.Strict
123123

124+
import Data.List.NonEmpty (nonEmpty)
125+
import qualified Data.List.NonEmpty as NonEmpty
124126
import Data.Kind (Type)
125127
import Data.Functor.Const (Const(..))
126128
import Data.Text (Text)
@@ -158,12 +160,17 @@ selectWith :: forall be db res
158160
, HasQBuilder be, Projectible be res )
159161
=> With be db (Q be db QBaseScope res) -> SqlSelect be (QExprToIdentity res)
160162
selectWith (CTE.With mkQ) =
161-
let (q, (recursiveness, ctes)) = evalState (runWriterT mkQ) 0
162-
in case recursiveness of
163-
CTE.Nonrecursive -> SqlSelect (withSyntax ctes
164-
(buildSqlQuery "t" q))
165-
CTE.Recursive -> SqlSelect (withRecursiveSyntax ctes
166-
(buildSqlQuery "t" q))
163+
let (q, (recursiveness, mctes)) = evalState (runWriterT mkQ) 0
164+
in case (recursiveness, nonEmpty mctes) of
165+
(CTE.Nonrecursive, Just ctes) -> SqlSelect (withSyntax (NonEmpty.toList ctes)
166+
(buildSqlQuery "t" q))
167+
(CTE.Recursive, Just ctes) -> SqlSelect (withRecursiveSyntax (NonEmpty.toList ctes)
168+
(buildSqlQuery "t" q))
169+
-- If there are no subqueries, we don't want to generate
170+
-- an empty 'WITH' statement, which would be malformed.
171+
--
172+
-- see: https://github.com/haskell-beam/beam/issues/760
173+
(_, Nothing) -> SqlSelect (buildSqlQuery "t" q)
167174

168175
-- | Convenience function to generate a 'SqlSelect' that looks up a table row
169176
-- given a primary key.

beam-core/beam-core.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: beam-core
2-
version: 0.11.1.0
2+
version: 0.11.2.0
33
synopsis: Type-safe, feature-complete SQL query and manipulation interface for Haskell
44
description: Beam is a Haskell library for type-safe querying and manipulation of SQL databases.
55
Beam is modular and supports various backends. In order to use beam, you will need to use

beam-duckdb/tests/Database/Beam/DuckDB/Test/Query.hs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
module Database.Beam.DuckDB.Test.Query (tests) where
1313

1414
import Control.Monad (void)
15+
import Data.Functor ((<&>))
1516
import Data.Int (Int32)
1617
import Data.List (nub, nubBy, sort, sortOn)
1718
import Data.Text (Text)
@@ -130,6 +131,7 @@ tests =
130131
testGroup
131132
"CTE"
132133
[ testCommonTableExpression,
134+
testEmptyCommonTableExpression,
133135
testMultipleCommonTableExpressions,
134136
testRecursiveCommonTableExpression
135137
],
@@ -421,6 +423,29 @@ testCommonTableExpression = testCase "Non-recursive common table expression" $ d
421423

422424
rows @?= [("Alice", 3), ("Bob", 1)]
423425

426+
-- Regression test for https://github.com/haskell-beam/beam/issues/760
427+
--
428+
-- Note that this is testing functionality from 'beam-core', but the MockSqlBackend
429+
-- isn't powerful enough to express this test
430+
testEmptyCommonTableExpression :: TestTree
431+
testEmptyCommonTableExpression = testCase "No common table expression in `selectWith` is equivalent to `select`" $ do
432+
let users =
433+
[ User 1 "Alice" 30,
434+
User 2 "Bob" 25,
435+
User 3 "Charlie" 35
436+
]
437+
withTestDb users [] [] $ \conn ->
438+
do
439+
rows <-
440+
runBeamDuckDB conn
441+
$ runSelectReturningList
442+
. selectWith
443+
. pure
444+
$ all_ (_dbUsers testDb)
445+
<&> _userName
446+
447+
rows @?= ["Alice", "Bob", "Charlie"]
448+
424449
testMultipleCommonTableExpressions :: TestTree
425450
testMultipleCommonTableExpressions = testCase "Multiple common table expressions" $ do
426451
let users =

beam-postgres/ChangeLog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.6.2.0
2+
3+
## Bug fixes
4+
5+
* Fixed an issue where using `pgSelectWith` with no common-table expressions
6+
would lead to an invalid SQL query at runtime.
7+
18
# 0.6.1.0
29

310
## Added features

beam-postgres/Database/Beam/Postgres/Full.hs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import Control.Monad.Free.Church
7171
import Control.Monad.State.Strict (evalState)
7272
import Control.Monad.Writer (runWriterT)
7373

74+
import Data.List.NonEmpty (nonEmpty)
75+
import qualified Data.List.NonEmpty as NonEmpty
7476
import Data.Kind (Type)
7577
import Data.Proxy (Proxy(..))
7678
import qualified Data.Text as T
@@ -289,18 +291,21 @@ lateral_ using mkSubquery = do
289291
--
290292
-- @beam-core@ offers 'selectWith' to produce a top-level 'SqlSelect'
291293
-- but these cannot be turned into 'Q' objects for use within joins.
292-
--
293-
-- The 'pgSelectWith' function is more flexible and indeed
294-
-- 'selectWith' for @beam-postgres@ is equivalent to se
294+
-- The 'pgSelectWith' function is more flexible.
295295
pgSelectWith :: forall db s res
296296
. Projectible Postgres res
297297
=> With Postgres db (Q Postgres db s res) -> Q Postgres db s res
298298
pgSelectWith (CTE.With mkQ) =
299-
let (q, (recursiveness, ctes)) = evalState (runWriterT mkQ) 0
299+
let (q, (recursiveness, mctes)) = evalState (runWriterT mkQ) 0
300300
fromSyntax tblPfx =
301-
case recursiveness of
302-
CTE.Nonrecursive -> withSyntax ctes (buildSqlQuery tblPfx q)
303-
CTE.Recursive -> withRecursiveSyntax ctes (buildSqlQuery tblPfx q)
301+
case (recursiveness, nonEmpty mctes) of
302+
(CTE.Nonrecursive, Just ctes) -> withSyntax (NonEmpty.toList ctes) (buildSqlQuery tblPfx q)
303+
(CTE.Recursive, Just ctes) -> withRecursiveSyntax (NonEmpty.toList ctes) (buildSqlQuery tblPfx q)
304+
-- If there are no subqueries, we don't want to generate
305+
-- an empty 'WITH' statement, which would be malformed.
306+
--
307+
-- see: https://github.com/haskell-beam/beam/issues/760
308+
(_, Nothing) -> buildSqlQuery tblPfx q
304309
in Q (liftF (QAll (\tblPfx tName ->
305310
let (_, names) = mkFieldNames @Postgres @res (qualifiedField tName)
306311
in fromTable (PgTableSourceSyntax $
@@ -309,7 +314,7 @@ pgSelectWith (CTE.With mkQ) =
309314
(\tName ->
310315
let (projection, _) = mkFieldNames @Postgres @res (qualifiedField tName)
311316
in projection)
312-
(\_ -> Nothing)
317+
(const Nothing)
313318
snd))
314319

315320
-- | By default, Postgres will throw an error when a conflict is detected. This

beam-postgres/beam-postgres.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: beam-postgres
2-
version: 0.6.1.0
2+
version: 0.6.2.0
33
synopsis: Connection layer between beam and postgres
44
description: Beam driver for <https://www.postgresql.org/ PostgreSQL>, an advanced open-source RDBMS
55
homepage: https://haskell-beam.github.io/beam/user-guide/backends/beam-postgres

0 commit comments

Comments
 (0)