packages feed

project-m36-1.0.0: src/bin/SQL/Interpreter/Select.hs

{-# LANGUAGE TypeFamilies #-}
module SQL.Interpreter.Select where
import ProjectM36.Interpreter
import ProjectM36.SQL.Select
import Text.Megaparsec
import Text.Megaparsec.Char
import Control.Monad.Combinators.Expr as E
import SQL.Interpreter.Base
import Data.Text (Text, splitOn)
import qualified Data.Text as T
import Data.Functor
import Data.Char (isLetter)
import qualified Data.List.NonEmpty as NE


parseSelect :: Text -> Either ParserError Select
parseSelect = parse (selectP <* semi <* eof) "<interactive>"

parseQuery :: Text -> Either ParserError Query
parseQuery = parse (queryP <* semi <* eof) "<interactive>"
  
queryP :: Parser Query
queryP = E.makeExprParser queryTermP queryOpP
  where
    queryTermP = (QuerySelect <$> selectP) <|>
                (QueryValues <$> valuesP) <|>
                (QueryTable <$> tableP)
    queryOpP = [[infixOpP "union" UnionQueryOperator,
                 infixOpP "intersect" IntersectQueryOperator,
                 infixOpP "except" ExceptQueryOperator
                ]]
    infixOpP nam op =
      E.InfixL $ do
        reserved nam
        pure (QueryOp op)

valuesP :: Parser [[ScalarExpr]]
valuesP = do
  reserved "values"
  sepByComma1 tupleP

tupleP :: Parser [ScalarExpr]
tupleP = parens (sepByComma1 scalarExprP)

tableP :: Parser TableName
tableP = do
  reserved "table"
  tableNameP

tableNameP :: Parser TableName
tableNameP = TableName <$> qualifiedNameP'
  
selectP :: Parser Select
selectP = do
  withClause' <- optional withP
  reserved "select"
--  distinctOptions
  projection <- selectItemListP
  tExpr <- optional tableExprP
  pure (Select { distinctness = Nothing,
                 projectionClause = projection,
                 tableExpr = tExpr,
                 withClause = withClause'
               })
  
  
selectItemListP :: Parser [SelectItem]
selectItemListP = sepBy selectItemP comma

selectItemP :: Parser SelectItem
selectItemP = (,) <$> scalarExprP <*> optional (reserved "as" *> columnAliasP)


tableExprP :: Parser TableExpr
tableExprP =
  TableExpr <$> fromP <*> optional whereP <*> option [] groupByP <*> optional havingP <*> option [] orderByP <*> limitP <*> offsetP

fromP :: Parser [TableRef]
fromP = reserved "from" *> (concat <$> sepByComma trefs)
  where
    trefs = (:) <$> nonJoinTref <*> many joinP
    nonJoinTref = choice [parens $ QueryTableRef <$> selectP,
                          try (AliasedTableRef <$> simpleRef <*> (reserved "as" *> tableAliasP)),
                          simpleRef]
    simpleRef = SimpleTableRef <$> tableNameP
    joinP = do
      joinType <- joinTypeP
      tref <- nonJoinTref
      case joinType of -- certain join types require join conditions, others not
        InnerJoin -> InnerJoinTableRef tref <$> joinConditionP
        RightOuterJoin -> RightOuterJoinTableRef tref <$> joinConditionP
        LeftOuterJoin -> LeftOuterJoinTableRef tref <$> joinConditionP
        FullOuterJoin -> FullOuterJoinTableRef tref <$> joinConditionP
        CrossJoin -> pure $ CrossJoinTableRef tref
        NaturalJoin -> pure $ NaturalJoinTableRef tref
      
joinConditionP :: Parser JoinCondition
joinConditionP = do
  (JoinOn <$> (reserved "on" *> (JoinOnCondition <$> scalarExprP))) <|>
   JoinUsing <$> (reserved "using" *> parens (sepBy1 unqualifiedColumnNameP comma))

joinTypeP :: Parser JoinType
joinTypeP = choice [reserveds "cross join" $> CrossJoin,
                    reserveds "inner join" $> InnerJoin,
                    (reserveds "left outer join" <|>
                     reserveds "left join") $> LeftOuterJoin,
                    (reserveds "right outer join" <|>
                     reserveds "right join") $> RightOuterJoin,
                     (reserveds "full join" <|>
                      reserveds "full outer join") $> FullOuterJoin,
                     (reserved "inner join" <|> reserveds "join") $> InnerJoin,
                     reserved "natural join" $> NaturalJoin]
  

whereP :: Parser RestrictionExpr
whereP = reserved "where" *> (RestrictionExpr <$> scalarExprP)

groupByP :: Parser [GroupByExpr]
groupByP =
  reserveds "group by" *> sepBy1 (GroupByExpr <$> scalarExprP) comma

havingP :: Parser HavingExpr
havingP = reserved "having" *> (HavingExpr <$> scalarExprP)

orderByP :: Parser [SortExpr]
orderByP =
  reserveds "order by" *> sepByComma1 (SortExpr <$> scalarExprP <*> optional directionP <*> optional nullsOrderP)
  where
    directionP = (reserved "asc" $> Ascending) <|>
                 (reserved "desc" $> Descending)
    nullsOrderP = (reserveds "nulls first" $> NullsFirst) <|>
                  (reserveds "nulls last" $> NullsLast)
  
nameP :: Parser Text
nameP = (quotedIdentifier <|> identifier) <* spaceConsumer

qualifiedNameP' :: Parser [Text]
qualifiedNameP' = sepBy1 nameP (symbol ".")

columnAliasP :: Parser ColumnAlias
columnAliasP = ColumnAlias <$> (quotedIdentifier <|> identifier)

tableAliasP :: Parser TableAlias
tableAliasP = TableAlias <$> (quotedIdentifier <|> identifier)

unqualifiedColumnNameP :: Parser UnqualifiedColumnName
unqualifiedColumnNameP = UnqualifiedColumnName <$> nameP

scalarExprP :: QualifiedNameP a => Parser (ScalarExprBase a)
scalarExprP = E.makeExprParser scalarTermP scalarExprOp

scalarExprOp :: QualifiedNameP a => [[E.Operator Parser (ScalarExprBase a)]]
scalarExprOp =
  [[qComparisonOp],
   [prefixSymbol "+",
    prefixSymbol "-"],
    [binarySymbolL "^"],
    map binarySymbolL ["\"","*","%"],
    map binarySymbolL ["+","-"],
    [binarySymbolR "||",
     prefixSymbol "~",
     binarySymbolR "&",
     binarySymbolR "|"],
    [binarySymbolN "like",
     E.Postfix $ try inSuffixP,
     E.Postfix betweenSuffixP
     --binarySymbolsN ["not", "like"]
     ],
    map binarySymbolN [">=","<=","!=","<>","=", "<",">"],
    [postfixKeywords ["is","null"], postfixKeywords ["is","not","null"]],
{-    [binarySymbolsN ["is", "distinct", "from"],
     binarySymbolsN ["is", "not", "distinct", "from"]],-}
    [binarySymbolL "and"],
    [prefixSymbol "not"],
    [binarySymbolL "or"]
    -- AT TIME ZONE
    ]
 where
   prefixSymbol s = E.Prefix $ PrefixOperator <$> qualifiedOperatorP s
   binarySymbolL s = E.InfixL $ binary s
   binary s = do
     op <- qualifiedOperatorP s
     pure (`BinaryOperator` op)
   binarySymbolR s = E.InfixR $ binary s
   binarySymbolN s = E.InfixN $ binary s
   qComparisonOp = E.Postfix $ try quantifiedComparisonSuffixP
   postfixKeywords kws = E.Postfix $ do
     try $ reserveds' kws
     pure (\a -> PostfixOperator a (OperatorName kws))
                     

qualifiedOperatorP :: Text -> Parser OperatorName
qualifiedOperatorP sym =
  OperatorName <$> segmentsP (splitOn "." sym) <* spaceConsumer
  where
    isLetters :: Text -> Bool
    isLetters = T.all isLetter
    segmentsP :: [Text] -> Parser [Text]
    -- simple case for non-string-based operators
    segmentsP [op] | not (isLetters op) = qualifiedNameSegment op $> [op]
    segmentsP segments = case segments of
      [] -> fail "empty operator"
      [seg] -> do
        final <- try (qualifiedNameSegment seg <* notFollowedBy alphaNumChar)
        pure [final]
      (seg:remainder) -> do
        first <- qualifiedNameSegment seg
        _ <- char '.'
        rem' <- segmentsP remainder
        pure (first:rem')
    

betweenSuffixP :: QualifiedNameP a => Parser (ScalarExprBase a -> ScalarExprBase a)
betweenSuffixP = do
  reserved "between"
  arg1 <- scalarExprP
  reserved "and"
  arg2 <- scalarExprP
  pure (\x -> BetweenOperator x arg1 arg2)

inSuffixP :: QualifiedNameP a => Parser (ScalarExprBase a -> ScalarExprBase a)
inSuffixP = do
  matchIn <|> matchNotIn
  where
    matchIn = do
      reserved "in"
      pred' <- inPredicateValue
      pure (\sexpr -> InExpr In sexpr pred')
    matchNotIn = do
      reserved "not" >> reserved "in"
      pred' <- inPredicateValue      
      pure (\sexpr -> InExpr NotIn sexpr pred')
    inPredicateValue = parens ((InQueryExpr <$> selectP) <|>
                                (InList <$> sepBy1 scalarExprP comma)) <|>
                       InScalarExpr <$> scalarExprP

  
quantifiedComparisonSuffixP :: QualifiedNameP a => Parser (ScalarExprBase a -> ScalarExprBase a)
quantifiedComparisonSuffixP = do
  op <- comparisonOperatorP
  quantOp <- (reserved "any" $> QCAny) <|>
             (reserved "some" $> QCSome) <|>
             (reserved "all" $> QCAll)
  subq <- parens selectP
  pure (\sexpr -> QuantifiedComparison { qcExpr = sexpr,
                                         qcOperator = op,
                                         qcPredicate = quantOp,
                                         qcQuery = subq })

comparisonOperatorP :: Parser ComparisonOperator
comparisonOperatorP = choice (map (\(match', op) -> reserved match' $> op) ops)
    where ops =[(">", OpGT),
                 ("<", OpLT),
                 ("=", OpEQ),
                 (">=", OpGTE),
                 ("<=", OpLTE),
                 ("<>", OpNE),
                 ("!=", OpNE)]

simpleLiteralP :: Parser (ScalarExprBase a)
simpleLiteralP = try doubleLiteralP <|> integerLiteralP <|> booleanLiteralP <|> stringLiteralP <|> nullLiteralP

doubleLiteralP :: Parser (ScalarExprBase a)
doubleLiteralP = DoubleLiteral <$> double

booleanLiteralP :: Parser (ScalarExprBase a)
booleanLiteralP = BooleanLiteral <$> ((reserved "true" $> True) <|> (reserved "false" $> False))

integerLiteralP :: Parser (ScalarExprBase a)
integerLiteralP = IntegerLiteral <$> integer

stringLiteralP :: Parser (ScalarExprBase a)
stringLiteralP = StringLiteral <$> stringP
  where
    stringP = do
      void $ char '\''
      stringEndP
    stringEndP = do
      capture <- T.pack <$> manyTill printChar (char '\'')
      choice [char '\'' *> (do
                               rest <- stringEndP
                               pure $ T.concat [capture, "'",rest]), --quoted quote
              pure capture
             ] <* spaceConsumer

nullLiteralP :: Parser (ScalarExprBase a)
nullLiteralP = 
  reserved "NULL" $> NullLiteral
  
scalarTermP :: QualifiedNameP a => Parser (ScalarExprBase a)
scalarTermP = choice [
  existsP,  
  simpleLiteralP,
    --,subQueryExpr
    caseExprP,
    --,cast
--    subquery,
--    pseudoArgFunc, -- includes NOW, NOW(), CURRENT_USER, TRIM(...), etc.
    scalarFunctionP,
    Identifier <$> qualifiedNameP
    ]
  <?> "scalar expression"

caseExprP :: QualifiedNameP a => Parser (ScalarExprBase a)
caseExprP = do
  let whenThenClause = do
        reserved "when"
        cond <- scalarExprP
        reserved "then"
        result <- scalarExprP
        pure (cond, result)
      elseClause = do
        reserved "else"
        scalarExprP
  reserved "case"
  conditionals <- some whenThenClause
  mElse <- optional elseClause
  reserved "end"
  pure (CaseExpr { caseWhens = conditionals,
                   caseElse = mElse })
       
scalarFunctionP :: QualifiedNameP a => Parser (ScalarExprBase a)
scalarFunctionP =
  try $
  FunctionApplication <$> functionNameP <*> parens (sepByComma scalarExprP)

existsP :: Parser (ScalarExprBase a)
existsP = do
  reserved "exists"
  ExistsExpr <$> parens selectP

-- used to distinguish between sections which may include an asterisk and those which cannot
class QualifiedNameP a where
  qualifiedNameP :: Parser a

-- | col, table.col, table.*, *
instance QualifiedNameP ColumnProjectionName where
  qualifiedNameP = columnProjectionNameP

instance QualifiedNameP ColumnName where
  qualifiedNameP = columnNameP

columnNameP :: Parser ColumnName
columnNameP = 
  ColumnName <$> sepBy1 nameP (char '.') <* spaceConsumer    

columnProjectionNameP :: Parser ColumnProjectionName
columnProjectionNameP =
  ColumnProjectionName <$> sepBy1 ((ProjectionName <$> nameP) <|> (char '*' $> Asterisk)) (char '.') <* spaceConsumer
  
functionNameP :: Parser FuncName
functionNameP = do
  FuncName <$> sepBy1 nameP (char '.') <* spaceConsumer

withExprAliasP :: Parser WithExprAlias
withExprAliasP = WithExprAlias <$> nameP

limitP :: Parser (Maybe Integer)
limitP = optional (reserved "limit" *> integer)

offsetP :: Parser (Maybe Integer)
offsetP = optional (reserved "offset" *> integer)

withP :: Parser WithClause
withP = do
  reserved "with"
  recursive <- try (reserved "recursive" $> True) <|> pure False  
  wExprs <- sepByComma1 $ do
    wName <- withExprAliasP
    reserved "as"
    wSelect <- parens selectP
    pure (WithExpr wName wSelect)
  pure (WithClause recursive (NE.fromList wExprs))