packages feed

preql-0.4: src/Preql/QuasiQuoter/Syntax/Printer.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}

-- | Print the types in Syntax as valid SQL.  The emphasis is on
-- queries to send to the database, not on legibilty; no extra whitespace is introduced.

module Preql.QuasiQuoter.Syntax.Printer where

import Preql.Imports
import Preql.QuasiQuoter.Syntax.Name
import Preql.QuasiQuoter.Syntax.Syntax as Syn hiding (select)

import Data.Data
import Data.List (intersperse)
import GHC.Generics
import Language.Haskell.TH.Syntax (Lift(..))
import Prelude hiding (GT, LT, lex)

import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as B
import qualified Data.Text.Lazy.Builder as TLB
import qualified Data.Text.Lazy.Builder.Int as B
import qualified Data.Text.Lazy.Builder.RealFloat as B

quote :: B.Builder -> B.Builder
quote s = "'" <> s <> "'"

doubleQuote :: B.Builder -> B.Builder
doubleQuote s = "\"" <> s <> "\""

parens :: B.Builder -> B.Builder
parens s = "(" <> s <> ")"

parensIf :: Bool -> B.Builder -> B.Builder
parensIf cond inner = if cond then parens inner else inner

spaceAfter :: B.Builder -> B.Builder
spaceAfter = (<> " ")

class FormatSql a where
    fmt :: a -> B.Builder
    fmt = fmtPrec 0

    fmtPrec :: Int -> a -> B.Builder
    fmtPrec _ = fmt

formatAsString :: FormatSql a => a -> String
formatAsString = TL.unpack . TLB.toLazyText . fmt

formatAsByteString :: FormatSql a => a -> ByteString
formatAsByteString = T.encodeUtf8 . formatAsText

formatAsText :: FormatSql a => a -> T.Text
formatAsText = TL.toStrict . TLB.toLazyText . fmt

instance FormatSql Name where
    -- TODO enclose keywoards &c in double quotes
    fmt = B.fromText . getName

instance FormatSql Literal where
    fmt (I i)     = B.decimal i
    fmt (F x)     = B.realFloat x
    fmt (T t)     = quote (B.fromText t)
    fmt (B True)  = "true"
    fmt (B False) = "false"
    fmt Null = "null"

instance FormatSql Statement where
    fmt (QI insert) = fmt insert
    fmt (QD delete) = fmt delete
    fmt (QU update) = fmt update
    fmt (QS select) = fmt select

commas :: (FormatSql a, Foldable f) => f a -> B.Builder
commas = fmtList ", "

spaces :: (FormatSql a, Foldable f) => f a -> B.Builder
spaces = fmtList " "

fmtList :: (FormatSql a, Foldable f) => B.Builder -> f a -> B.Builder
fmtList sep as = mconcat (intersperse sep (map fmt (toList as)))

unlessEmpty :: (B.Builder -> B.Builder) -> B.Builder -> B.Builder
unlessEmpty _ "" = ""
unlessEmpty f x = f x

optList :: FormatSql a => B.Builder -> [a] -> B.Builder
optList _ [] = ""
optList prepend as = prepend <> commas as

-- TODO replace all calls to @opt@ with @opt'@, rename
opt :: FormatSql a => B.Builder -> Maybe a -> B.Builder
opt _ Nothing = ""
opt prepend (Just a) = prepend <> fmt a

opt' :: FormatSql a => B.Builder -> Int -> Maybe a -> B.Builder
opt' _ _ Nothing = ""
opt' prepend p (Just a) = prepend <> fmtPrec p a

instance FormatSql B.Builder where
    fmt = id

instance FormatSql Insert where
    fmt Insert{table, columns, values} =
        "INSERT INTO " <> fmt table <> " (" <> commas columns <>
        ") VALUES (" <> commas values <> ")"

instance FormatSql Delete where
    fmt Delete{table, conditions} = "DELETE FROM " <> fmt table <> wh where
      wh = case conditions of
          Nothing         -> ""
          Just conditions' -> " WHERE " <> fmt conditions'

instance FormatSql Setting where
    fmt (Setting column rhs) = fmt column <> "=" <> fmt rhs

instance FormatSql Update where
    fmt Update{table, settings, conditions} =
        "UPDATE " <> fmt table <> " SET " <> commas settings <> wh
      where wh = case conditions of
                Nothing         -> ""
                Just conditions' -> " WHERE " <> fmt conditions'

instance FormatSql Expr where
    fmtPrec _ (Lit lit)  = fmt lit
    fmtPrec _ (CRef name) = fmt name
    fmtPrec _ (NumberedParam i) = B.fromString ('$' : show i)
    fmtPrec _ (HaskellParam txt) = "${" <> B.fromText txt <> "}"
    fmtPrec p (BinOp op l r) = let (assoc, p1) = binOpPrec op
      in parensIf (p > p1) $ case assoc of
          LeftAssoc -> fmtPrec p1 l <> " " <> fmt op <> " " <> fmtPrec (p1 + 1) r
          RightAssoc -> fmtPrec (p1 + 1) l <> " " <> fmt op <> " " <> fmtPrec p1 r
          NonAssoc -> fmtPrec (p1 + 1) l <> " " <> fmt op <> " " <> fmtPrec (p1 + 1) r
    fmtPrec p (Unary op expr) = case op of
        Negate -> parensIf (p > 15) ("-" <>  fmtPrec 15 expr)
        Not -> parensIf (p > 5) ("NOT " <> fmtPrec 5 expr)
        IsNull -> parensIf (p > 7) (fmtPrec 8 expr <> " IS NULL")
        NotNull -> parensIf (p > 7) (fmtPrec 8 expr <> " IS NOT NULL")
    -- This looks funky, but seems to match the parser
    fmtPrec _ (Indirection e indirects) =
      let m_parens = case e of
            NumberedParam _ -> id
            CRef _ -> id
            _ -> parens
      in m_parens (fmt e) <> fmtIndirections indirects
    fmtPrec _ (SelectExpr stmt) = parens (fmt stmt)
    fmtPrec p (L likeE) = fmtPrec p likeE
    fmtPrec _ (Fun f) = fmt f
    fmtPrec _ (Cas c) = fmt c

fmtIndirections :: Foldable f => f Indirection -> TLB.Builder
fmtIndirections = foldMap (("." <>) . fmt)

instance FormatSql BinOp where
    fmt op = case op of
        Mul      -> "*"
        Div      -> "/"
        Add      -> "+"
        Sub      -> "-"
        Exponent -> "^"
        Mod -> "%"
        Eq    -> "="
        LT    -> "<"
        LTE   -> "<="
        GT    -> ">"
        GTE   -> ">="
        NEq   -> "!="
        IsDistinctFrom -> "IS DISTINCT FROM"
        IsNotDistinctFrom -> "IS NOT DISTINCT FROM"
        And -> "AND"
        Or -> "OR"

data Assoc = LeftAssoc | RightAssoc | NonAssoc
  deriving (Show, Eq, Enum, Bounded, Data, Lift, Generic)

binOpPrec :: BinOp -> (Assoc, Int)
binOpPrec op = case op of
  Or -> (LeftAssoc, 3)
  And -> (LeftAssoc, 4)
  IsDistinctFrom -> (NonAssoc, 7)
  IsNotDistinctFrom -> (NonAssoc, 7)
  Eq -> (NonAssoc, 8)
  LT -> (NonAssoc, 8)
  LTE -> (NonAssoc, 8)
  GT -> (NonAssoc, 8)
  GTE -> (NonAssoc, 8)
  NEq -> (NonAssoc, 8)
  Add -> (LeftAssoc, 12)
  Sub -> (LeftAssoc, 12)
  Mul -> (LeftAssoc, 13)
  Div -> (LeftAssoc, 13)
  Mod -> (LeftAssoc, 13)
  Exponent -> (LeftAssoc, 14)

setOpPrec :: SetOp -> Int
setOpPrec op = case op of
  Union -> 1
  Except -> 1
  Intersect -> 2

instance FormatSql LikeE where
  -- Expr L puts parens around if needed
    fmtPrec p LikeE{op, string, likePattern, escape, invert} = parensIf (p > likePrec) $
        fmtPrec 10 string <> (if invert then " NOT" else "")
        <> op' <> fmtPrec 10 likePattern <> opt' " ESCAPE " 10 escape
      where
        likePrec = if invert then 5 else 9
        op' = case op of
              Like -> " LIKE "
              ILike -> " ILIKE "
              Similar -> " SIMILAR TO "

instance FormatSql SelectStmt where
    fmtPrec _ (SelectValues values) = "VALUES " <> commas (fmap (parens . commas) values)
    fmtPrec _ (Simple un) = fmt un
    fmtPrec p (S ss so) = let topLevel = parensIf (p > 0) (fmtPrec 1 ss <> fmt so) in
      case withClause so of
        Nothing -> topLevel
        Just ctes -> fmt ctes <> " " <> topLevel
    fmtPrec p (Set op distinct l r) = parensIf (p > q) $
      fmtPrec q l <> " " <> fmt op <> d <> fmtPrec (q + 1) r
      where
        q = setOpPrec op
        d = case distinct of
                All -> " ALL "
                Distinct -> " "

instance FormatSql Select where
    fmt Select{targetList, from, distinct, whereClause, groupBy, having, window}
        = "SELECT " <> m_distinct <> commas (fmt <$> targetList) <> " FROM " <> commas from
          <> opt " WHERE " whereClause
          <> optList " GROUP BY " groupBy
          <> opt " HAVING " having
          <> optList " WINDOW " window
        where
          m_distinct = maybe "" (spaceAfter . fmt) distinct

instance FormatSql SelectOptions where
  -- ignore WithClause here; handle it in SelectStmt so we can put it before the top query
    fmt SelectOptions{sortBy, offset, limit, locking} =
        optList " ORDER BY " sortBy
        <> unlessEmpty (" " <>) (spaces locking) -- no commas
        <> opt " LIMIT " limit
        <> opt " OFFSET " offset

instance FormatSql WithClause where
  fmt With {commonTables, recursive} =
    "WITH" <> recursive' <> commas commonTables
    where recursive' = case recursive of
            Recursive -> " RECURSIVE "
            NotRecursive -> " "

instance FormatSql Materialized where
  fmt Materialized = "MATERIALIZED"
  fmt NotMaterialized = "NOT MATERIALIZED"
  fmt MaterializeDefault = ""

instance FormatSql CTE where
  fmt CommonTableExpr {name, aliases, materialized, query} =
    fmt name <> unlessEmpty parens (commas aliases)
    <> unlessEmpty spacesAround (fmt materialized) <> parens (fmt query)
    where
      spacesAround s = " " <> s <> " "

instance FormatSql TableRef where
    fmtPrec p (J jt) = fmtPrec p jt
    fmtPrec p (As jt alias) = parensIf (p > 1) $ fmtPrec 1 jt <> " AS " <> fmt alias
    fmtPrec _ (SubSelect stmt alias) = parens (fmt stmt) <> " AS " <> fmt alias

instance FormatSql Alias where
    fmt (Alias name []) = fmt name
    fmt (Alias name columns) = fmt name <> parens (commas columns)

instance FormatSql JoinedTable where
    fmtPrec _ (Table name) = fmt name
    fmtPrec p (CrossJoin l r) = parensIf (p > 0) $ fmtPrec 0 l <> " CROSS JOIN " <> fmtPrec 1 r
    fmtPrec p (Join Inner Natural l r) = parensIf (p > 0) $ fmtPrec 0 l <> " NATURAL JOIN " <> fmtPrec 1 r
    fmtPrec p (Join ty Natural l r) = parensIf (p > 0) $ fmtPrec 0 l <> " NATURAL" <> fmt ty <> "JOIN " <> fmtPrec 1 r
    fmtPrec p (Join ty (Using cols) l r) = parensIf (p > 0) $ fmtPrec 0 l <> fmt ty <> " JOIN " <> fmtPrec 1 r <> " USING " <> parens (commas cols)
    fmtPrec p (Join ty (On expr) l r) = parensIf (p > 0) $ fmtPrec 0 l <> fmt ty <> " JOIN " <> fmtPrec 0 r <> " ON " <> fmtPrec 0 expr

instance FormatSql JoinType where
    fmt Inner = " INNER "
    fmt LeftJoin = " LEFT "
    fmt RightJoin = " RIGHT "
    fmt Full = " FULL "

instance FormatSql DistinctClause where
    fmt DistinctAll = "DISTINCT"
    fmt (DistinctOn expr) = "DISTINCT ON " <> parens (commas expr)

instance FormatSql SortBy where
    fmt (SortBy expr order nulls) = fmt expr <> " " <> fmt order <> fmt nulls

instance FormatSql SortOrderOrUsing where
    fmt (SortOrder order) = fmt order
    fmt (SortUsing op) = "USING " <> fmt op

instance FormatSql SortOrder where
    -- leading space
    fmt Ascending = " ASC"
    fmt Descending = " DESC"
    fmt DefaultSortOrder = ""

instance FormatSql NullsOrder where
    -- leading space
    fmt NullsFirst = " NULLS FIRST"
    fmt NullsLast = " NULLS LAST"
    fmt NullsOrderDefault = ""

instance FormatSql Locking where
    fmt Locking{strength, tables, wait} =
        " " <> fmt strength <> optList " OF " tables <> " " <> fmt wait

instance FormatSql LockingStrength where
    fmt ForUpdate = "FOR UPDATE"
    fmt ForNoKeyUpdate = "FOR NO KEY UPDATE"
    fmt ForShare = "FOR SHARE"
    fmt ForKeyShare = "FOR KEY SHARE"

instance FormatSql LockWait where
    fmt LockWaitError = "NOWAIT"
    fmt LockWaitSkip = "SKIP LOCKED"
    fmt LockWaitBlock = ""

instance FormatSql SetOp where
    fmt Union = "UNION"
    fmt Intersect = "INTERSECT"
    fmt Except = "EXCEPT"

instance FormatSql ResTarget where
    fmt Star = "*"
    fmt (Column expr Nothing) = fmt expr
    fmt (Column expr (Just name)) = fmt expr <> " AS " <> fmt name

-- instance FormatSql ColumnRef where
--     fmt ColumnRef {value, name} = fmt value <> case name of
--         Nothing -> ""
--         Just n -> "." <> fmt n

instance FormatSql WindowDef where
  fmt (WindowDef name spec) = fmt name <> " AS " <> fmt spec

instance FormatSql WindowSpec where
    fmt WindowSpec { refName, partitionClause, orderClause }
        = "(" <> mconcat [m_refName, m_partition, m_order ] <> ")" where
      m_refName = maybe "" fmt refName
      m_partition = case partitionClause of
          [] -> ""
          _ -> " PARTITION BY " <> commas (fmt <$> partitionClause)
      m_order = case orderClause of
          [] -> ""
          _ -> " ORDER BY " <> commas (fmt <$> orderClause)

instance FormatSql FunctionApplication where
  fmt FApp {..} = fmt name <> fmtIndirections indirection
    <> fmt arguments  <> withinGroup'
    <> maybe "" (\fc -> " FILTER " <> parens ("WHERE " <> fmt fc)) filterClause
    <> over'
    where
      withinGroup' = case withinGroup of
        [] -> ""
        _ -> "WITHIN GROUP " <> parens ("ORDER BY " <> commas withinGroup)
      over' = case over of
        (Window (WindowSpec Nothing [] [])) -> ""
        (WindowName alias) -> "OVER " <> fmt alias
        Window WindowSpec {refName, partitionClause, orderClause} ->
          "OVER " <> parens
              (opt "" refName
               <> optList " PARTITION BY " partitionClause
               <> optList " ORDER BY " orderClause)

instance FormatSql FunctionArguments where
  fmt StarArg = "(*)"
  fmt NoArgs = "()"
  fmt (Args ArgsList{..}) = parens (distinct' <> commas arguments <> sortBy') where
    distinct' = if distinct then "DISTINCT " else ""
    sortBy' = optList " ORDER BY " sortBy

instance FormatSql Argument where
  fmt (E e) = fmt e
  fmt (Named name e) = fmt name <> " => " <> fmt e

instance FormatSql Case where
  fmt Case { whenClause, implicitArg, elseClause } =
   "CASE" <> opt " " implicitArg <> whenClauses' <> opt " ELSE " elseClause <> " END"
    where whenClauses' = spaces [ " WHEN " <> fmt condition <> " THEN " <> fmt result
                                | (condition, result) <- whenClause ]