graphql-engine/server/src-lib/Hasura/SQL/DML.hs

722 lines
17 KiB
Haskell

{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
module Hasura.SQL.DML where
import Hasura.Prelude
import Hasura.SQL.Types
import Data.String (fromString)
import Language.Haskell.TH.Syntax (Lift)
import qualified Data.HashMap.Strict as HM
import qualified Data.Text.Extended as T
import qualified Text.Builder as TB
infixr 6 <->
(<->) :: TB.Builder -> TB.Builder -> TB.Builder
(<->) l r = l <> TB.char ' ' <> r
{-# INLINE (<->) #-}
paren :: TB.Builder -> TB.Builder
paren t = TB.char '(' <> t <> TB.char ')'
{-# INLINE paren #-}
data Select
= Select
{ selDistinct :: !(Maybe DistinctExpr)
, selExtr :: ![Extractor]
, selFrom :: !(Maybe FromExp)
, selWhere :: !(Maybe WhereFrag)
, selGroupBy :: !(Maybe GroupByExp)
, selHaving :: !(Maybe HavingExp)
, selOrderBy :: !(Maybe OrderByExp)
, selLimit :: !(Maybe LimitExp)
, selOffset :: !(Maybe OffsetExp)
} deriving (Show, Eq)
mkSelect :: Select
mkSelect = Select Nothing [] Nothing
Nothing Nothing Nothing
Nothing Nothing Nothing
newtype LimitExp
= LimitExp SQLExp
deriving (Show, Eq)
instance ToSQL LimitExp where
toSQL (LimitExp se) =
"LIMIT" <-> toSQL se
newtype OffsetExp
= OffsetExp SQLExp
deriving (Show, Eq)
instance ToSQL OffsetExp where
toSQL (OffsetExp se) =
"OFFSET" <-> toSQL se
newtype OrderByExp
= OrderByExp [OrderByItem]
deriving (Show, Eq)
data OrderByItem
= OrderByItem
{ oColumn :: !SQLExp
, oType :: !(Maybe OrderType)
, oNulls :: !(Maybe NullsOrder)
} deriving (Show, Eq)
instance ToSQL OrderByItem where
toSQL (OrderByItem e ot no) =
toSQL e <-> toSQL ot <-> toSQL no
data OrderType = OTAsc
| OTDesc
deriving (Show, Eq, Lift)
instance ToSQL OrderType where
toSQL OTAsc = "ASC"
toSQL OTDesc = "DESC"
data NullsOrder
= NFirst
| NLast
deriving (Show, Eq, Lift)
instance ToSQL NullsOrder where
toSQL NFirst = "NULLS FIRST"
toSQL NLast = "NULLS LAST"
instance ToSQL OrderByExp where
toSQL (OrderByExp l) =
"ORDER BY" <-> (", " <+> l)
newtype GroupByExp
= GroupByExp [SQLExp]
deriving (Show, Eq)
instance ToSQL GroupByExp where
toSQL (GroupByExp idens) =
"GROUP BY" <-> (", " <+> idens)
newtype FromExp
= FromExp [FromItem]
deriving (Show, Eq)
instance ToSQL FromExp where
toSQL (FromExp items) =
"FROM" <-> (", " <+> items)
mkIdenFromExp :: (IsIden a) => a -> FromExp
mkIdenFromExp a =
FromExp [FIIden $ toIden a]
mkSimpleFromExp :: QualifiedTable -> FromExp
mkSimpleFromExp qt =
FromExp [FISimple qt Nothing]
mkSelFromExp :: Bool -> Select -> TableName -> FromItem
mkSelFromExp isLateral sel tn =
FISelect (Lateral isLateral) sel alias
where
alias = Alias $ toIden tn
mkRowExp :: [Extractor] -> SQLExp
mkRowExp extrs = let
innerSel = mkSelect { selExtr = extrs }
innerSelName = TableName "e"
-- SELECT r FROM (SELECT col1, col2, .. ) AS r
outerSel = mkSelect
{ selExtr = [Extractor (SERowIden $ toIden innerSelName) Nothing]
, selFrom = Just $ FromExp
[mkSelFromExp False innerSel innerSelName]
}
in
SESelect outerSel
newtype HavingExp
= HavingExp BoolExp
deriving (Show, Eq)
instance ToSQL HavingExp where
toSQL (HavingExp be) =
"HAVING" <-> toSQL be
newtype WhereFrag
= WhereFrag { getWFBoolExp :: BoolExp }
deriving (Show, Eq)
instance ToSQL WhereFrag where
toSQL (WhereFrag be) =
"WHERE" <-> paren (toSQL be)
instance ToSQL Select where
toSQL sel =
"SELECT"
<-> toSQL (selDistinct sel)
<-> (", " <+> selExtr sel)
<-> toSQL (selFrom sel)
<-> toSQL (selWhere sel)
<-> toSQL (selGroupBy sel)
<-> toSQL (selHaving sel)
<-> toSQL (selOrderBy sel)
<-> toSQL (selLimit sel)
<-> toSQL (selOffset sel)
mkSIdenExp :: (IsIden a) => a -> SQLExp
mkSIdenExp = SEIden . toIden
mkQIdenExp :: (IsIden a, IsIden b) => a -> b -> SQLExp
mkQIdenExp q t = SEQIden $ mkQIden q t
data Qual
= QualIden !Iden
| QualTable !QualifiedTable
| QualVar !T.Text
deriving (Show, Eq)
mkQual :: QualifiedTable -> Qual
mkQual = QualTable
instance ToSQL Qual where
toSQL (QualIden i) = toSQL i
toSQL (QualTable qt) = toSQL qt
toSQL (QualVar v) = TB.text v
mkQIden :: (IsIden a, IsIden b) => a -> b -> QIden
mkQIden q t = QIden (QualIden (toIden q)) (toIden t)
data QIden
= QIden !Qual !Iden
deriving (Show, Eq)
instance ToSQL QIden where
toSQL (QIden qual iden) =
mconcat [toSQL qual, TB.char '.', toSQL iden]
newtype SQLOp
= SQLOp {sqlOpTxt :: T.Text}
deriving (Show, Eq)
incOp :: SQLOp
incOp = SQLOp "+"
mulOp :: SQLOp
mulOp = SQLOp "*"
jsonbConcatOp :: SQLOp
jsonbConcatOp = SQLOp "||"
jsonbDeleteOp :: SQLOp
jsonbDeleteOp = SQLOp "-"
jsonbDeleteAtPathOp :: SQLOp
jsonbDeleteAtPathOp = SQLOp "#-"
newtype AnnType
= AnnType {unAnnType :: T.Text}
deriving (Show, Eq)
intType :: AnnType
intType = AnnType "int"
textType :: AnnType
textType = AnnType "text"
textArrType :: AnnType
textArrType = AnnType "text[]"
jsonType :: AnnType
jsonType = AnnType "json"
jsonbType :: AnnType
jsonbType = AnnType "jsonb"
data CountType
= CTStar
| CTSimple ![PGCol]
| CTDistinct ![PGCol]
deriving(Show, Eq)
instance ToSQL CountType where
toSQL CTStar = "*"
toSQL (CTSimple cols) =
paren $ ", " <+> cols
toSQL (CTDistinct cols) =
"DISTINCT" <-> paren (", " <+> cols)
data SQLExp
= SEPrep !Int
| SELit !T.Text
| SEUnsafe !T.Text
| SESelect !Select
| SEStar
| SEIden !Iden
-- iden and row identifier are distinguished for easier rewrite rules
| SERowIden !Iden
| SEQIden !QIden
| SEFnApp !T.Text ![SQLExp] !(Maybe OrderByExp)
| SEOpApp !SQLOp ![SQLExp]
| SETyAnn !SQLExp !AnnType
| SECond !BoolExp !SQLExp !SQLExp
| SEBool !BoolExp
| SEExcluded !T.Text
| SEArray ![SQLExp]
| SECount !CountType
deriving (Show, Eq)
newtype Alias
= Alias { getAlias :: Iden }
deriving (Show, Eq, Hashable)
instance IsIden Alias where
toIden (Alias iden) = iden
instance ToSQL Alias where
toSQL (Alias iden) = "AS" <-> toSQL iden
toAlias :: (IsIden a) => a -> Alias
toAlias = Alias . toIden
countStar :: SQLExp
countStar = SECount CTStar
instance ToSQL SQLExp where
toSQL (SEPrep argNumber) =
TB.char '$' <> fromString (show argNumber)
toSQL (SELit tv) =
TB.text $ pgFmtLit tv
toSQL (SEUnsafe t) =
TB.text t
toSQL (SESelect se) =
paren $ toSQL se
toSQL SEStar =
TB.char '*'
toSQL (SEIden iden) =
toSQL iden
toSQL (SERowIden iden) =
toSQL iden
toSQL (SEQIden qIden) =
toSQL qIden
-- https://www.postgresql.org/docs/10/static/sql-expressions.html#SYNTAX-AGGREGATES
toSQL (SEFnApp name args mObe) =
TB.text name <> paren ((", " <+> args) <-> toSQL mObe)
toSQL (SEOpApp op args) =
paren (sqlOpTxt op <+> args)
toSQL (SETyAnn e ty) =
paren (toSQL e) <> "::" <> TB.text (unAnnType ty)
toSQL (SECond cond te fe) =
"CASE WHEN" <-> toSQL cond <->
"THEN" <-> toSQL te <->
"ELSE" <-> toSQL fe <->
"END"
toSQL (SEBool be) = toSQL be
toSQL (SEExcluded t) = "EXCLUDED."
<> toSQL (PGCol t)
toSQL (SEArray exps) = "ARRAY" <> TB.char '['
<> (", " <+> exps) <> TB.char ']'
toSQL (SECount ty) = "COUNT" <> paren (toSQL ty)
intToSQLExp :: Int -> SQLExp
intToSQLExp =
SEUnsafe . T.pack . show
annotateExp :: SQLExp -> PGColType -> SQLExp
annotateExp sqlExp =
SETyAnn sqlExp . AnnType . T.pack . show
data Extractor = Extractor !SQLExp !(Maybe Alias)
deriving (Show, Eq)
mkSQLOpExp
:: SQLOp
-> SQLExp -- lhs
-> SQLExp -- rhs
-> SQLExp -- result
mkSQLOpExp op lhs rhs = SEOpApp op [lhs, rhs]
mkColDefValMap :: [PGCol] -> HM.HashMap PGCol SQLExp
mkColDefValMap cols =
HM.fromList $ zip cols (repeat $ SEUnsafe "DEFAULT")
handleIfNull :: SQLExp -> SQLExp -> SQLExp
handleIfNull l e = SEFnApp "coalesce" [e, l] Nothing
applyJsonBuildObj :: [SQLExp] -> SQLExp
applyJsonBuildObj args =
SEFnApp "json_build_object" args Nothing
applyRowToJson :: [Extractor] -> SQLExp
applyRowToJson extrs =
SEFnApp "row_to_json" [mkRowExp extrs] Nothing
getExtrAlias :: Extractor -> Maybe Alias
getExtrAlias (Extractor _ ma) = ma
mkAliasedExtr :: (IsIden a, IsIden b) => a -> Maybe b -> Extractor
mkAliasedExtr t = mkAliasedExtrFromExp (mkSIdenExp t)
mkAliasedExtrFromExp :: (IsIden a) => SQLExp -> Maybe a -> Extractor
mkAliasedExtrFromExp sqlExp ma = Extractor sqlExp (aliasF <$> ma)
where
aliasF = Alias . toIden
mkExtr :: (IsIden a) => a -> Extractor
mkExtr t = Extractor (mkSIdenExp t) Nothing
instance ToSQL Extractor where
toSQL (Extractor ce mal) =
toSQL ce <-> toSQL mal
data DistinctExpr
= DistinctSimple
| DistinctOn ![SQLExp]
deriving (Show, Eq)
instance ToSQL DistinctExpr where
toSQL DistinctSimple = "DISTINCT"
toSQL (DistinctOn exps) =
"DISTINCT ON" <-> paren ("," <+> exps)
data FromItem
= FISimple !QualifiedTable !(Maybe Alias)
| FIIden !Iden
| FISelect !Lateral !Select !Alias
| FIJoin !JoinExpr
deriving (Show, Eq)
mkSelFromItem :: Select -> Alias -> FromItem
mkSelFromItem = FISelect (Lateral False)
mkLateralFromItem :: Select -> Alias -> FromItem
mkLateralFromItem = FISelect (Lateral True)
instance ToSQL FromItem where
toSQL (FISimple qt mal) =
toSQL qt <-> toSQL mal
toSQL (FIIden iden) =
toSQL iden
toSQL (FISelect mla sel al) =
toSQL mla <-> paren (toSQL sel) <-> toSQL al
toSQL (FIJoin je) =
toSQL je
newtype Lateral = Lateral Bool
deriving (Show, Eq)
instance ToSQL Lateral where
toSQL (Lateral True) = "LATERAL"
toSQL (Lateral False) = mempty
data JoinExpr
= JoinExpr
{ tjeLeft :: !FromItem
, tjeType :: !JoinType
, tjeRight :: !FromItem
, tjeJC :: !JoinCond
} deriving (Show, Eq)
instance ToSQL JoinExpr where
toSQL je =
toSQL (tjeLeft je)
<-> toSQL (tjeType je)
<-> toSQL (tjeRight je)
<-> toSQL (tjeJC je)
data JoinType
= Inner
| LeftOuter
| RightOuter
| FullOuter
deriving (Eq, Show)
instance ToSQL JoinType where
toSQL Inner = "INNER JOIN"
toSQL LeftOuter = "LEFT OUTER JOIN"
toSQL RightOuter = "RIGHT OUTER JOIN"
toSQL FullOuter = "FULL OUTER JOIN"
data JoinCond
= JoinOn !BoolExp
| JoinUsing ![PGCol]
deriving (Show, Eq)
instance ToSQL JoinCond where
toSQL (JoinOn be) =
"ON" <-> paren (toSQL be)
toSQL (JoinUsing cols) =
"USING" <-> paren ("," <+> cols)
data BoolExp
= BELit !Bool
| BEBin !BinOp !BoolExp !BoolExp
| BENot !BoolExp
| BECompare !CompareOp !SQLExp !SQLExp
| BENull !SQLExp
| BENotNull !SQLExp
| BEExists !Select
| BEIN !SQLExp ![SQLExp]
deriving (Show, Eq)
-- removes extraneous 'AND true's
simplifyBoolExp :: BoolExp -> BoolExp
simplifyBoolExp be = case be of
BEBin AndOp e1 e2 ->
let e1s = simplifyBoolExp e1
e2s = simplifyBoolExp e2
in if
| e1s == BELit True -> e2s
| e2s == BELit True -> e1s
| otherwise -> BEBin AndOp e1s e2s
BEBin OrOp e1 e2 ->
let e1s = simplifyBoolExp e1
e2s = simplifyBoolExp e2
in if
| e1s == BELit False -> e2s
| e2s == BELit False -> e1s
| otherwise -> BEBin OrOp e1s e2s
e -> e
mkExists :: FromItem -> BoolExp -> BoolExp
mkExists fromItem whereFrag =
BEExists mkSelect
{ selExtr = [Extractor (SEUnsafe "1") Nothing]
, selFrom = Just $ FromExp $ pure fromItem
, selWhere = Just $ WhereFrag whereFrag
}
instance ToSQL BoolExp where
toSQL (BELit True) = TB.text $ T.squote "true"
toSQL (BELit False) = TB.text $ T.squote "false"
toSQL (BEBin bo bel ber) =
paren (toSQL bel) <-> toSQL bo <-> paren (toSQL ber)
toSQL (BENot be) =
"NOT" <-> paren (toSQL be)
toSQL (BECompare co vl vr) =
paren (toSQL vl) <-> toSQL co <-> paren (toSQL vr)
toSQL (BENull v) =
paren (toSQL v) <-> "IS NULL"
toSQL (BENotNull v) =
paren (toSQL v) <-> "IS NOT NULL"
toSQL (BEExists sel) =
"EXISTS " <-> paren (toSQL sel)
-- special case to handle lhs IN (exp1, exp2)
toSQL (BEIN vl exps) =
paren (toSQL vl) <-> toSQL SIN <-> paren (", " <+> exps)
data BinOp = AndOp
| OrOp
deriving (Show, Eq)
instance ToSQL BinOp where
toSQL AndOp = "AND"
toSQL OrOp = "OR"
data CompareOp
= SEQ
| SGT
| SLT
| SIN
| SNE
| SLIKE
| SNLIKE
| SILIKE
| SNILIKE
| SSIMILAR
| SNSIMILAR
| SGTE
| SLTE
| SNIN
| SContains
| SContainedIn
| SHasKey
| SHasKeysAny
| SHasKeysAll
deriving (Eq)
instance Show CompareOp where
show = \case
SEQ -> "="
SGT -> ">"
SLT -> "<"
SIN -> "IN"
SNE -> "<>"
SGTE -> ">="
SLTE -> "<="
SNIN -> "NOT IN"
SLIKE -> "LIKE"
SNLIKE -> "NOT LIKE"
SILIKE -> "ILIKE"
SNILIKE -> "NOT ILIKE"
SSIMILAR -> "SIMILAR TO"
SNSIMILAR -> "NOT SIMILAR TO"
SContains -> "@>"
SContainedIn -> "<@"
SHasKey -> "?"
SHasKeysAny -> "?|"
SHasKeysAll -> "?&"
instance ToSQL CompareOp where
toSQL = fromString . show
buildInsVal :: PGCol -> Int -> (PGCol, SQLExp)
buildInsVal colName argNumber =
(colName, SEPrep argNumber)
data SQLDelete
= SQLDelete
{ delTable :: !QualifiedTable
, delUsing :: !(Maybe UsingExp)
, delWhere :: !(Maybe WhereFrag)
, delRet :: !(Maybe RetExp)
} deriving (Show, Eq)
data SQLUpdate
= SQLUpdate
{ upTable :: !QualifiedTable
, upSet :: !SetExp
, upFrom :: !(Maybe FromExp)
, upWhere :: !(Maybe WhereFrag)
, upRet :: !(Maybe RetExp)
} deriving (Show, Eq)
newtype SetExp = SetExp [SetExpItem]
deriving (Show, Eq)
newtype SetExpItem = SetExpItem (PGCol, SQLExp)
deriving (Show, Eq)
buildSEI :: PGCol -> Int -> SetExpItem
buildSEI colName argNumber =
SetExpItem (colName, SEPrep argNumber)
buildSEWithExcluded :: [PGCol] -> SetExp
buildSEWithExcluded cols = SetExp $ flip map cols $
\col -> SetExpItem (col, SEExcluded $ getPGColTxt col)
newtype UsingExp = UsingExp [TableName]
deriving (Show, Eq)
instance ToSQL UsingExp where
toSQL (UsingExp tables)
= "USING" <-> "," <+> tables
newtype RetExp = RetExp [Extractor]
deriving (Show, Eq)
selectStar :: Extractor
selectStar = Extractor SEStar Nothing
returningStar :: RetExp
returningStar = RetExp [selectStar]
instance ToSQL RetExp where
toSQL (RetExp [])
= mempty
toSQL (RetExp exps)
= "RETURNING" <-> (", " <+> exps)
instance ToSQL SQLDelete where
toSQL sd = "DELETE FROM"
<-> toSQL (delTable sd)
<-> toSQL (delUsing sd)
<-> toSQL (delWhere sd)
<-> toSQL (delRet sd)
instance ToSQL SQLUpdate where
toSQL a = "UPDATE"
<-> toSQL (upTable a)
<-> toSQL (upSet a)
<-> toSQL (upFrom a)
<-> toSQL (upWhere a)
<-> toSQL (upRet a)
instance ToSQL SetExp where
toSQL (SetExp cvs) =
"SET" <-> ("," <+> cvs)
instance ToSQL SetExpItem where
toSQL (SetExpItem (col, val)) =
toSQL col <-> "=" <-> toSQL val
data SQLConflictTarget
= SQLColumn ![PGCol]
| SQLConstraint !ConstraintName
deriving (Show, Eq)
instance ToSQL SQLConflictTarget where
toSQL (SQLColumn cols) = "("
<-> ("," <+> cols)
<-> ")"
toSQL (SQLConstraint cons) = "ON CONSTRAINT" <-> toSQL cons
data SQLConflict
= DoNothing !(Maybe SQLConflictTarget)
| Update !SQLConflictTarget !SetExp
deriving (Show, Eq)
instance ToSQL SQLConflict where
toSQL (DoNothing Nothing) = "ON CONFLICT DO NOTHING"
toSQL (DoNothing (Just ct)) = "ON CONFLICT"
<-> toSQL ct
<-> "DO NOTHING"
toSQL (Update ct ex) = "ON CONFLICT"
<-> toSQL ct <-> "DO UPDATE"
<-> toSQL ex
data SQLInsert = SQLInsert
{ siTable :: !QualifiedTable
, siCols :: ![PGCol]
, siTuples :: ![[SQLExp]]
, siConflict :: !(Maybe SQLConflict)
, siRet :: !(Maybe RetExp)
} deriving (Show, Eq)
instance ToSQL SQLInsert where
toSQL si =
let insTuples = flip map (siTuples si) $ \tupVals ->
"(" <-> (", " <+> tupVals) <-> ")"
insConflict = maybe "" toSQL
in "INSERT INTO"
<-> toSQL (siTable si)
<-> "("
<-> (", " <+> siCols si)
<-> ") VALUES"
<-> (", " <+> insTuples)
<-> insConflict (siConflict si)
<-> toSQL (siRet si)
data CTE
= CTESelect !Select
| CTEInsert !SQLInsert
| CTEUpdate !SQLUpdate
| CTEDelete !SQLDelete
deriving (Show, Eq)
instance ToSQL CTE where
toSQL = \case
CTESelect q -> toSQL q
CTEInsert q -> toSQL q
CTEUpdate q -> toSQL q
CTEDelete q -> toSQL q
data SelectWith
= SelectWith
{ swCTEs :: [(Alias, CTE)]
, swSelect :: !Select
} deriving (Show, Eq)
instance ToSQL SelectWith where
toSQL (SelectWith ctes sel) =
"WITH " <> (", " <+> map f ctes) <-> toSQL sel
where
f (Alias al, q) = toSQL al <-> "AS" <-> paren (toSQL q)