Haskell Relational Record's Query-Building DSL

2014-12-25 (Updated 2014-12-29)

Kei Hibino

Rough Design

Query DSL Basics

$$\{ (x, y) | x \in X, y \in Y, \pi_1(x) = \pi_2(y) \}$$

[ (x, y) | x <- xs, y <- ys, fst x == snd y ] -- Comprehension

do { x <- xs; y <- ys; guard (fst x == snd y); return (x, y) } -- List Monad

Building a joined query like list comprehension or list Monad:

personAndBirthday :: Relation () (Person, Birthday)
personAndBirthday =  relation $ do
  p <- query person    -- Join product accumulated
  b <- query birthday
  on $ p ! Person.name' .=. b ! Birthday.name'
  return $ p >< b

Typing

A simple and useful method:

State Stack

Haskell Relational Record's query-building DSL accumulates various context in a state monad context stack.

Contexts in State

Join Product

query :: (MonadQualify ConfigureQuery m, MonadQuery m)
      => Relation () r
      -> m (Projection Flat r)

-- Used for outer join
queryMaybe :: (MonadQualify ConfigureQuery m, MonadQuery m)
           => Relation () r
           -> m (Projection Flat (Maybe r))

on :: MonadQuery m => Projection Flat (Maybe Bool) -> m ()

'query' and 'queryMaybe' return a Projection type of table form results.

SELECT .. FROM ...
            -- Accumulating uniquely qualified
            -- ( like 'as T0', 'as T1' ... )
            -- table forms of SQL FROM clause

Join Example

personAndBirthdayL :: Relation () (Person, Maybe Birthday)
personAndBirthdayL =  relation $ do
  p <- query person
  b <- queryMaybe birthday
  on $ just (p ! Person.name') .=. b ?! Birthday.name'
  return $ p >< b

generates left-joined SQL:

SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
  FROM PUBLIC.person T0 LEFT JOIN
       PUBLIC.birthday T1
       ON (T0.name = T1.name)

Aggregation

groupBy :: MonadAggregate m
        => Projection Flat r
        -- ^ Projection to add into group by
        -> m (Projection Aggregated r)
        -- ^ Result context and aggregated projection

count :: Projection Flat a -> Projection Aggregated Int64
max'  :: Ord a
      => Projection Flat a -> Projection Aggregated (Maybe a)

'groupBy' can be used under only 'MonadAggregate' monad constraint, narrower than 'MonadQuery'.

'groupBy' returns a Projection value with an Aggregated context type:

SELECT .. GROUP BY ...
                -- Accumulating keys
                -- of SQL GROUP BY clause

Aggregation Example

agesOfFamilies :: Relation () (String, Maybe Int32)
agesOfFamilies =  aggregateRelation $ do
  my <- query myTable
  gFam <- groupBy $ my ! family'     -- Specify grouping key

  return $ gFam >< sum' (my ! age')  -- Aggregated results

sums ages per family.

Generated SQL:

SELECT ALL T0.family AS f0, SUM (T0.age) AS f1
  FROM PUBLIC.my_table T0
  GROUP BY T0.family

Restrict

restrict :: MonadRestrict c m
         => Projection c (Maybe Bool)
         -> m ()

wheres :: MonadRestrict Flat m
       => Projection Flat (Maybe Bool)
       -> m ()

adds a WHERE clause restriction:

SELECT .. WHERE x AND y AND ...
             -- Accumulating AND predicates
             -- of SQL WHERE clause

Restrict

restrict :: MonadRestrict c m
         => Projection c (Maybe Bool)
         -> m ()

having :: MonadRestrict Aggregated m
       => Projection Aggregated (Maybe Bool)
       -> m ()

adds a HAVING clause restriction, in which only Projection type values with aggregated context are allowed:

SELECT .. HAVING x AND y AND ...
              -- Accumulating AND predicates
              -- of SQL HAVING clause

Restrict Example

sameBirthdayHeisei' :: Relation () (Day, Int64)
sameBirthdayHeisei' =  aggregateRelation $ do
  p <- query person
  b <- query birthday
  on $ p ! Person.name' .=. b ! Birthday.name'
  wheres $ b ! Birthday.day' .>=. value (fromGregorian 1989 1 8)
  gbd <- groupBy $ b ! Birthday.day'
  having $ count (p ! Person.name') .>. value 1
  return $ gbd >< count (p ! Person.name')

counts people with the same birthday, who were born in the Heisei period.

Generated SQL:

SELECT ALL T1.day AS f0, COUNT (T0.name) AS f1
  FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
    ON (T0.name = T1.name)
 WHERE (T1.day >= DATE '1989-01-08')
 GROUP BY T1.day HAVING (COUNT (T0.name) > 1)

Restrict Example

Generated SQL:

SELECT ALL T1.day AS f0, COUNT (T0.name) AS f1
  FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
    ON (T0.name = T1.name)
 WHERE (T1.day >= DATE '1989-01-08')
 GROUP BY T1.day HAVING (COUNT (T0.name) > 1)

Restrict Example

sameBirthdayHeisei :: Relation () (Day, Int64)
sameBirthdayHeisei =  aggregateRelation $ do
  p <- query person
  b <- query birthday
  on $ p ! Person.name' .=. b ! Birthday.name'
  let birthDay = b ! Birthday.day'
  wheres $ birthDay .>=. value (fromGregorian 1989 1 8)
  gbd <- groupBy birthDay
  let personCount = count $ p ! Person.name'
  having $ personCount .>. value 1
  return $ gbd >< personCount

binds using let.

Ordering

orderBy :: Monad m
        => Projection c t
        -- ^ Ordering terms to add
        -> Order
        -- ^ Order direction -- Asc | Desc
        -> Orderings c m ()
        -- ^ Result context with ordering

Only Projection type values with specified (Flat, Aggregated, ...) context are allowed.

SELECT .. ORDER BY ...
                -- Accumulating terms of ORDER BY clause

Ordering Example

personAndBirthdayO :: Relation () (Person, Birthday)
personAndBirthdayO =  relation $ do
  p <- query person
  b <- query birthday
  on $ p ! Person.name' .=. b ! Birthday.name'
  orderBy (b ! Birthday.day') Asc  -- Specify ordering key
  orderBy (p ! Person.name') Asc
  return $ p >< b

orders by birthday and then name:

SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
  FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
    ON (T0.name = T1.name)
  ORDER BY T1.day ASC, T0.name ASC

Ordering Example

birthdayHeiseiDesc :: Relation () (Day, Int64)
birthdayHeiseiDesc =  aggregateRelation $ do
  p <- query person
  b <- query birthday
  on $ p ! Person.name' .=. b ! Birthday.name'
  let birthDay = b ! Birthday.day'
  wheres $ birthDay .>=. value (fromGregorian 1989 1 8)
  gbd <- groupBy birthDay
  let personCount = count $ p ! Person.name'
  orderBy personCount Desc
  return $ gbd >< personCount

orders by the number of people born on the same Heisei period dates:

SELECT ALL T1.day AS f0, COUNT (T0.name) AS f1
  FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
    ON (T0.name = T1.name)
 WHERE (T1.day >= DATE '1989-01-08')
 GROUP BY T1.day ORDER BY COUNT (T0.name) DESC

Ordering Example

SELECT ALL T1.day AS f0, COUNT (T0.name) AS f1
  FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
    ON (T0.name = T1.name)
 WHERE (T1.day >= DATE '1989-01-08')
 GROUP BY T1.day ORDER BY COUNT (T0.name) DESC

Other Features

Placeholders

specifyPerson :: Relation String (Person, Birthday)
specifyPerson =  relation' $ do
  pb <- query personAndBirthday -- Re-use predefined
  (ph, ()) <- placeholder
              (\ph' -> wheres $ pb ! fst' ! Person.name' .=. ph')
  return (ph, pb)

specifies a person name using a placeholder:

SELECT ALL T2.f0 AS f0, T2.f1 AS f1, T2.f2 AS f2,
           T2.f3 AS f3, T2.f4 AS f4
  FROM (SELECT ALL
               T0.name AS f0, T0.age AS f1, T0.address AS f2,
               T1.name AS f3, T1.day AS f4
          FROM PUBLIC.person T0 INNER JOIN
               PUBLIC.birthday T1
            ON (T0.name = T1.name)) T2
 WHERE (T2.f0 = ?)

Map SQL Values to a Haskell Record

Mapping to records using Applicative style:

(|$|) :: (ProjectableFunctor p, ProductConstructor (a -> b))
      => (a -> b)
      -> p a
      -> p b
(|*|) :: ProjectableApplicative p
      => p (a -> b)
      -> p a
      -> p b

Record Mapping - Projections

Assign record types to an SQL projection:

personAndBirthdayT :: Relation () PersonAndBirthday
personAndBirthdayT =  relation $ do
  p <- query person
  b <- query birthday
  wheres $ p ! Person.name' .=. b ! Birthday.name'
  -- Build record phantom type
  return $ PersonAndBirthday |$| p |*| b

(|$|) :: ProductConstructor (a -> b)
      => (a -> b) -> Projection c a -> Projection c b
(|*|) :: Projection c (a -> b) -> Projection c a -> Projection c b
SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
      FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
        ON (T0.name = T1.name)

Record Mapping - Column Selectors

Column selectors can be mapped to a record:

Birthday.day' :: Pi Birthday Day

uncurryPB :: Pi (Person, Birthday) PersonAndBirthday
uncurryPB =  PersonAndBirthday |$| fst' |*| snd'

(|$|) :: ProductConstructor (a -> b)
      => (a -> b) -> Pi r a -> Pi r b
(|*|) :: Pi r (a -> b) -> Pi r a -> Pi r b

Record Mapping - Placeholders

Placeholders can be mapped to a record:

personAndBirthdayP2 :: Relation Person PersonAndBirthday
personAndBirthdayP2 =  relation' $ do
  p <- query person
  b <- query birthday
  (ph0, ()) <- placeholder (\ph0' -> on $ p ! Person.name'     .=. ph0')
  (ph1, ()) <- placeholder (\ph1' -> on $ p ! Person.age'      .=. ph1')
  (ph2, ()) <- placeholder (\ph2' -> on $ p ! Person.address'  .=. ph2')

  return (Person |$| ph0 |*| ph1 |*| ph2,
          PersonAndBirthday |$| p |*| b)

(|$|) :: ProductConstructor (a -> b)
      => (a -> b) -> Placeholders a -> Placeholders b
(|*|) :: Placeholders (a -> b) -> Placeholders a -> Placeholders b

Generated SQL:

SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
      FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
        ON (((T0.name = ?) AND (T0.age = ?)) AND (T0.address = ?))

Record Mapping - Placeholders

Generated SQL:

SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
      FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
        ON (((T0.name = ?) AND (T0.age = ?)) AND (T0.address = ?))

Record Mapping - Record Placeholders

Record-typed placeholder:

placeholder :: (PersistableWidth t, Monad m)
            => (Projection c t -> m a) -> m (PlaceHolders t, a)

personAndBirthdayP :: Relation Person PersonAndBirthday
personAndBirthdayP =  relation' $ do
  p <- query person
  b <- query birthday
  (ph, ()) <- placeholder (\ph' -> wheres $ p .=. ph')
  return $ (ph, PersonAndBirthday |$| p |*| b)

row value of Placeholders:

SELECT ALL T0.name AS f0, T0.age AS f1, T0.address AS f2,
           T1.name AS f3, T1.day AS f4
      FROM PUBLIC.person T0 INNER JOIN PUBLIC.birthday T1
        ON ((T0.name, T0.age, T0.address) = (?, ?, ?))

Questions?

Window Function

Monadic-style window building:

ageRankOfFamilies :: Relation () ((Int64, String), Int32)
ageRankOfFamilies =  relation $ do
  my <- query myTable
  return $
    rank `over` do
      partitionBy $ my ! family'  -- Monad to build window
      orderBy (my ! age') Desc
    ><
    my ! family' >< my ! age'
SELECT ALL
       RANK() OVER (PARTITION BY T0.family
                    ORDER BY T0.age DESC) AS f0,
       T0.family AS f1, T0.age AS f2
  FROM PUBLIC.my_table T0

Discussion

Others

exists Operator

exists :: (SqlProjectable p, ProjectableShowSql p)
       => ListProjection (Projection Exists) r -> p (Maybe Bool)

queryList :: MonadQualify ConfigureQuery m
          => Relation () r
          -> m (ListProjection (Projection c) r)

in' Operator

in' :: (SqlProjectable p, ProjectableShowSql p)
    => p t -> ListProjection p t -> p (Maybe Bool)

queryList :: MonadQualify ConfigureQuery m
          => Relation () r
          -> m (ListProjection (Projection c) r)

values :: (ShowConstantTermsSQL t, SqlProjectable p)
       => [t] -> ListProjection p t

Scalar Queries

queryScalar :: (MonadQualify ConfigureQuery m, ScalarDegree r)
            => UniqueRelation () c r
            -> m (Projection c (Maybe r))