Skip to content

Incorrect queries generated when using nub_ or pgNubBy_ as source for window function #746

Open
@emilaxelsson

Description

@emilaxelsson

The code at the bottom queries the persons table to find each unique valid_from day paired with the next unique valid_from day. However, the generated queries use DISTINCT incorrectly.

This could be related to #531.

Expected result

The unique days are 2025-01-01, 2025-01-02 and 2025-01-03. Pairing each day with the next should give:

(2025-01-01,Just 2025-02-02)
(2025-01-02,Just 2025-03-03)
(2025-01-03,Nothing)

This is equivalent to what the following query produces:

WITH commits AS (
  SELECT DISTINCT valid_from FROM persons
)
SELECT valid_from, LEAD(valid_from)
OVER (ORDER BY valid_from ASC) FROM commits

Actual result with pgNubBy_ id

(2025-01-01,Just 2025-01-01)
(2025-01-02,Just 2025-01-02)
(2025-01-03,Just 2025-01-03)

Actual result with nub_

(2025-01-03,Nothing)
(2025-01-02,Just 2025-01-03)
(2025-01-01,Just 2025-01-01)
(2025-01-01,Just 2025-01-02)
(2025-01-03,Just 2025-01-03)
(2025-01-02,Just 2025-01-02)

Code

#!/usr/bin/env stack
{- stack script
    --snapshot lts-23.0
    --system-ghc
    --package beam-core
    --package beam-postgres
    --package postgresql-simple
    --package text
    --package time
-}

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

import Data.Coerce (coerce)
import Data.Text (Text)
import Data.Time.Calendar (Day)
import Database.Beam
import Database.Beam.Postgres
import Database.PostgreSQL.Simple (Query, execute_)

data PersonT f = Person
    { name :: C f Text
    , title :: C f Text
    , valid_from :: C f Day
    }
    deriving (Generic)

type Person = PersonT Identity

type PersonExpr s = PersonT (QExpr Postgres s)

deriving instance Show Person
deriving instance Eq Person

instance Beamable PersonT

instance Table PersonT where
    data PrimaryKey PersonT f = PersonKey (C f Text)
        deriving stock (Generic)
        deriving anyclass (Beamable)

    primaryKey Person{name} = PersonKey name

data Db f = Db
    { persons :: f (TableEntity PersonT)
    }
    deriving (Generic)

instance Database Postgres Db

db :: DatabaseSettings Postgres Db
db = defaultDbSettings

-- | All unique days from @persons@
commits :: Q Postgres Db s (QGenExpr QValueContext Postgres s Day)
-- commits = nub_ $ fmap valid_from $ all_ $ persons db
commits = pgNubBy_ id $ fmap valid_from $ all_ $ persons db

-- For fixing the type of `lead1_`; see <https://github.com/haskell-beam/beam/issues/745>
fixType :: QExpr Postgres s a -> QExpr Postgres s (Maybe a)
fixType = coerce

-- | The result of 'commits' each paired with the day of the next commit
commitsWithValidTo ::
    Q Postgres Db s
        ( QGenExpr QValueContext Postgres s Day
        , QGenExpr QValueContext Postgres s (Maybe Day)
        )
commitsWithValidTo =
    fmap fixType <$>
        withWindow_
            ( \valid_from ->
                frame_
                    noPartition_
                    (orderPartitionBy_ (asc_ valid_from))
                    noBounds_
            )
            ( \valid_from w ->
                (valid_from, lead1_ valid_from `over_` w)
            )
            commits

main = do
    conn <- connect defaultConnectInfo {connectDatabase = "test"}

    execute_ conn
        $ "create table if not exists persons"
        <> "(name varchar, title varchar, valid_from date)"
    execute_ conn
        $ "delete from persons"
    execute_ conn
        $ "insert into persons (name, title, valid_from) values"

        -- Commit 1
        <> "('Janne', 'Mechanic', '2025-01-01'),"
        <> "('Jenny', 'Consultant', '2025-01-01'),"

        -- Commit 2
        <> "('Janne', 'Cleaner', '2025-01-02'),"
        <> "('Jenny', 'Assistant', '2025-01-02'),"

        -- Commit 3
        <> "('Janne', 'CEO', '2025-01-03'),"
        <> "('Jenny', 'CFO', '2025-01-03')"

    allCommits <- runBeamPostgresDebug putStrLn conn $
        runSelectReturningList $ select commits

    allCommitsWithValidTo <- runBeamPostgresDebug putStrLn conn $
        runSelectReturningList $ select commitsWithValidTo


    putStrLn "----"
    mapM_ print allCommits
    putStrLn "----"
    mapM_ print allCommitsWithValidTo

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions