packages feed

lmonad-yesod-0.1.0.0: src/Database/LEsqueleto/LSql.hs

{-# LANGUAGE OverloadedStrings #-}

module Database.LEsqueleto.LSql where

import Control.Applicative
import Data.Attoparsec.Text
import qualified Data.Char as Char
import qualified Data.Text as Text

-- | Represent the AST for lsql statements. 
data Command = Command {
        commandSelect :: Select
      , commandTerms :: Terms
      , commandTables :: Tables
      , commandWhere :: Maybe Where
      , commandOrderBy :: Maybe OrderBy
      , commandLimit :: Maybe Limit
      , commandOffset :: Maybe Offset
    }
    deriving (Show)
data Select = Select | PSelect
    deriving (Eq, Show)

--data Terms = Term Term | Terms Terms Term | TermsAll
data Terms = Terms [Term] | TermsAll
    deriving (Show)

data Tables = Table String | Tables Tables Join String BExpr
    deriving (Show)
data Join = InnerJoin | LeftOuterJoin | RightOuterJoin | FullOuterJoin -- | CrossJoin
    deriving (Show)

data Where = Where BExpr
    deriving (Show)

data OrderBy = OrderBy [Order]
    deriving (Show)

data Order = OrderAsc Term | OrderDesc Term
    deriving (Show)

data Limit = Limit Integer
    deriving (Show)

data Offset = Offset Integer
    deriving (Show)

data Term = TermTF String TermField | TermF TermField
    deriving (Show)
data TermField = Field String | FieldAll

data BExpr = BExprAnd BExpr BExpr | BExprOr BExpr BExpr | BExprBinOp B BinOp B | BExprNull Term | BExprNotNull Term | BExprNot BExpr
    deriving (Show)

data BinOp = BinEq | BinNEq | BinGE | BinG | BinLE | BinL
    deriving (Show)

data B = BTerm Term | BAnti String | BConst C
    deriving (Show)
data C = CBool Bool | CString String | CInt Integer | CDouble Double
    deriving (Show)

instance Show TermField where
    show (Field s) = s
    show (FieldAll) = "*"

parseCommand :: Parser Command
parseCommand = do
    select <- parseSelect
    terms <- parseTerms
    _ <- parseFrom
    tables <- parseTables
    whereM <- parseWhere
    orderByM <- parseOrderBy
    limitM <- parseLimit
    offsetM <- parseOffset
    parseComma
    return $ Command select terms tables whereM orderByM limitM offsetM

    where
        takeNonSpace = takeWhile1 (not . Char.isSpace)

        takeAlphaNum = takeWhile1 Char.isAlphaNum
        takeUpperAlphaNum = do
            an <- takeAlphaNum
            return $ Text.map Char.toUpper an

        parseComma = do
            skipSpace
            -- Check for optional comma.
            commaM <- peekChar
            case commaM of
                Nothing ->
                    return ()
                Just c ->
                    if c == ';' then
                        return ()
                    else do
                        rest <- takeText
                        error $ "Error parsing from: `" ++ (Text.unpack rest) ++ "`"
            -- maybe (return ()) 
            -- try (char ';' >> skipSpace)
            -- end <- atEnd
            -- unless end $ do
            --     rest <- takeText
            --     error $ "Error parsing from: `"++ (Text.unpack rest) ++"`"

        parseSelect = do
            skipSpace
            select <- takeUpperAlphaNum
            case select of
                "SELECT" ->
                    return Select
                "PSELECT" ->
                    return PSelect
                _ ->
                    fail $ "Unknown keywork `" ++ (Text.unpack select) ++ "`. Use `SELECT` or `PSELECT`."

        parseTerms = 
            let parseTerms' = do
                  head <- parseTerm
                  tail <- (do
                        skipSpace
                        _ <- char ','
                        parseTerms'
                    ) <|> (return [])
                  return $ head:tail
            in
            (skipSpace >> char '*' >> (return TermsAll)) <|> (parseTerms' >>= return . Terms)
            
        parseFrom = do
            skipSpace
            asciiCI "FROM"

        parseTerm = do
            skipSpace
            ( do
                table <- takeAlphaNum
                _ <- char '.'
                field <- parseField
                return $ TermTF (Text.unpack table) field
              ) <|> (parseField >>= (return . TermF))

        -- Does not skip spaces!!
        parseField = (char '*' >> (return FieldAll)) <|> 
            ( takeAlphaNum >>= (return . Field. Text.unpack))

        parseTables = 
            let parseTables' acc = ( do
                    skipSpace
                    join <- ( asciiCI "INNER JOIN" >> (return InnerJoin)) <|> 
                      ( asciiCI "OUTER JOIN" >> (return LeftOuterJoin)) <|>
                      ( asciiCI "LEFT OUTER JOIN" >> (return LeftOuterJoin)) <|>
                      ( asciiCI "RIGHT OUTER JOIN" >> (return RightOuterJoin)) <|>
                      ( asciiCI "FULL OUTER JOIN" >> (return FullOuterJoin)) -- <|>
                      --( asciiCI "CROSS JOIN" >> (return CrossJoin))
                    skipSpace
                    table <- takeAlphaNum
                    skipSpace
                    _ <- asciiCI "ON"
                    bexpr <- parseBExpr
                    parseTables' $ Tables acc join (Text.unpack table) bexpr
                  ) <|> (return acc)
            in
            do
            skipSpace
            table <- takeAlphaNum
            parseTables' $ Table $ Text.unpack table

        parseWhere = ( do
            skipSpace
            _ <- asciiCI "WHERE"
            bexp <- parseBExpr
            return $ Just $ Where bexp
          ) <|> (return Nothing)
        
        parseOrderBy = ( do
            skipSpace
            _ <- asciiCI "ORDER BY"
            orders <- parseOrders
            return $ Just $ OrderBy orders
          ) <|> (return Nothing)
            
            where
                parseOrders = do
                    term <- parseTerm
                    order <- ( skipSpace >> asciiCI "ASC" >> (return OrderAsc)) <|> 
                        ( skipSpace >> asciiCI "DESC" >> (return OrderDesc)) <|> 
                        ( return OrderAsc)
                    tail <- ( do
                        skipSpace
                        _ <- asciiCI ","
                        parseOrders
                      ) <|> (return [])
                    return $ (order term):tail

        parseLimit = ( do
            skipSpace
            _ <- asciiCI "LIMIT"
            skipSpace
            limit <- decimal
            return $ Just $ Limit limit
          ) <|> (return Nothing)
        
        parseOffset = ( do
            skipSpace
            _ <- asciiCI "OFFSET"
            skipSpace
            limit <- decimal
            return $ Just $ Offset limit
          ) <|> (return Nothing)

        parseBExpr = do
            expr1 <- ( do
                skipSpace
                _ <- char '('
                res <- parseBExpr
                skipSpace
                _ <- char ')'
                return res
              ) <|> ( do
                skipSpace
                _ <- asciiCI "NOT"
                res <- parseBExpr
                return $ BExprNot res
              ) <|> ( do
                term <- parseTerm
                skipSpace
                _ <- asciiCI "IS NULL"
                return $ BExprNull term
              ) <|> ( do
                term <- parseTerm
                skipSpace
                _ <- asciiCI "IS NOT NULL"
                return $ BExprNotNull term
              ) <|> ( do
                b1 <- parseB
                op <- parseBOp
                b2 <- parseB
                return $ BExprBinOp b1 op b2
              )
            ( do
                skipSpace
                -- temp <- takeAlphaNum
                -- when (temp /= "where" && temp /= "and") $ 
                --     error $ "here: " ++ (show expr1) ++ " **** " ++ (Text.unpack temp)
                constr <- (asciiCI "AND" >> (return BExprAnd)) <|>
                    (asciiCI "OR" >> peekChar >>= (maybe (return BExprOr) $ \c -> 
                        if Char.isSpace c then
                            return BExprOr
                        else
                            fail "OR: Some other keyword"
                      ))
                expr2 <- parseBExpr
                return $ constr expr1 expr2
              ) <|> (return expr1)

          where
            parseSQLString = takeWhile1 (/= '\'')

            parseConst = skipSpace >> 
                ( asciiCI "TRUE" >> return (CBool True)) <|>
                ( asciiCI "FALSE" >> return (CBool False)) <|>
                ( do
                    _ <- char '\'' 
                    skipSpace
                    str <- parseSQLString
                    skipSpace
                    _ <- char '\'' 
                    return $ CString $ Text.unpack str
                ) <|> ( do
                    int <- signed decimal
                    next <- peekChar
                    case next of 
                        Just '.' ->
                            fail "this is a double"
                        _ ->
                            return $ CInt int
                ) <|>
                ( double >>= (return . CDouble))

            parseB = ( do
                skipSpace
                _ <- asciiCI "#{"
                skipSpace
                var <- takeWhile1 (/= '}')--takeNonSpace -- TODO: maybe make this into a [String] and stop at '}'
                skipSpace
                _ <- char '}'
                return $ BAnti $ Text.unpack var
              ) <|> ( do
                term <- parseTerm
                return $ BTerm term
              ) <|> ( do
                c <- parseConst
                return $ BConst c
              )

            parseBOp = do
                skipSpace
                op <- takeNonSpace
                return $ case op of
                    "==" -> BinEq
                    "!=" -> BinNEq
                    ">=" -> BinGE
                    ">" -> BinG
                    "<=" -> BinLE
                    "<" -> BinL
                    t -> 
                        error $ "Invalid binop `" ++ (Text.unpack t) ++ "`"
                -- >> ( asciiCI "==" >> return BinEq) <|> 
                -- ( asciiCI ">=" >> return BinGE) <|>
                -- ( char '>' >> return BinG) <|>
                -- ( asciiCI "<=" >> return BinLE) <|>
                -- ( char '<' >> return BinL) <|>
                -- ( takeAlphaNum >>= \t -> fail $ "Invalid binop `" ++ (Text.unpack t) ++ "`")