{-# LANGUAGE OverloadedStrings #-}
module Database.Selda.SQLite.Parser (colsFromQuery) where
import Control.Applicative
import Control.Monad (void, msum, MonadPlus (..))
import Data.Char (isSpace, isAlpha, isAlphaNum)
import Data.Maybe (isJust, catMaybes)
import Data.Text (Text)
import qualified Data.Text as Text
colsFromQuery :: Text -> [(Text, (Text, Bool))]
colsFromQuery = parse' parseCreateQueryCols
newtype Parser a = P { unP :: (Text -> Maybe (Text, a)) }
instance Functor Parser where
fmap f (P g) = P (fmap (fmap f) . g)
instance Applicative Parser where
pure x = P $ \t -> Just (t, x)
f <*> x = f >>= \f' -> fmap f' x
instance Alternative Parser where
empty = P $ const Nothing
P f <|> P g = P $ \s ->
case f s of
res@(Just _) -> res
_ -> g s
instance Monad Parser where
return = pure
P m >>= f = P $ \s -> do
case m s of
Just (rest, x) -> unP (f x) rest
_ -> Nothing
instance MonadPlus Parser where
mzero = empty
mplus = (<|>)
parse :: Parser a -> Text -> Maybe a
parse (P f) t = snd <$> f t
parse' :: Parser a -> Text -> a
parse' f t = maybe (error $ "no parse: '" ++ show t ++ "'") id $ parse f t
lowerText :: Text -> Parser ()
lowerText prefix = P $ \s ->
case Text.splitAt (Text.length prefix) s of
(prefix', rest) | prefix == Text.toLower prefix' -> Just (rest, ())
_ -> Nothing
charP :: (Char -> Bool) -> Parser Char
charP p = P $ \s ->
case Text.splitAt 1 s of
(prefix, rest) | Text.any p prefix -> Just (rest, Text.head prefix)
_ -> Nothing
char :: Char -> Parser Char
char c = charP (== c)
space :: Parser ()
space = void $ charP isSpace
spaces :: Parser ()
spaces = void $ some space
sepBy1 :: Parser s -> Parser a -> Parser [a]
sepBy1 sep p = do
x <- p
xs <- optional $ sep *> sepBy1 sep p
case xs of
Just xs' -> pure (x:xs')
_ -> pure [x]
commaSeparated :: Parser a -> Parser [a]
commaSeparated = sepBy1 (many space >> char ',' >> many space)
keywords :: [Text]
keywords = ["constraint", "unique", "primary key"]
parseCreateQueryCols :: Parser [(Text, (Text, Bool))]
parseCreateQueryCols = do
lowerText "create table"
spaces
void $ sqliteIdentifier
void $ many space
void $ char '('
cols <- commaSeparated parseCol <* many space
void $ char ')'
pure $ catMaybes cols
parseCol :: Parser (Maybe (Text, (Text, Bool)))
parseCol = do
decl <- constraint <|> column
pure $ case decl of
Right col -> Just col
_ -> Nothing
where
column = do
name <- sqliteIdentifier
spaces
ty <- sqliteIdentifier
void $ optional $ spaces *> lowerText "primary key"
isAuto <- optional $ spaces *> lowerText "autoincrement"
void $ many $ charP (\c -> isAlphaNum c || isSpace c)
void $ optional $ do
void $ char '('
void $ commaSeparated sqliteIdentifier
void $ char ')'
pure $ Right $ (name, (ty, isJust isAuto))
constraint = do
msum (map lowerText keywords)
void $ many $ msum
[ void sqliteIdentifier
, void $ do
void $ char '('
void $ commaSeparated sqliteIdentifier
void $ char ')'
, spaces
]
pure $ Left ()
sqliteIdentifier :: Parser Text
sqliteIdentifier = Text.pack <$> (quoted <|> unquoted)
where
unquoted = do
x <- charP $ \c -> isAlpha c || c == '_'
xs <- many $ charP $ \c -> isAlphaNum c || c == '_' || c == '$'
pure $ (x:xs)
quoted = char '"' *> many quotedChar <* char '"'
quotedChar = (char '"' >> char '"') <|> charP (/= '"')