postgresql-syntax-0.5.0.2: library-internal/PostgresqlSyntax/Helpers/Parsers.hs
{-# OPTIONS_GHC -Wno-redundant-constraints -Wno-missing-signatures -Wno-dodgy-imports #-}
module PostgresqlSyntax.Helpers.Parsers
( module PostgresqlSyntax.Extras.HeadedMegaparsec,
module PostgresqlSyntax.Helpers.Parsers,
)
where
import Control.Applicative.Combinators hiding (some)
import qualified Data.HashSet as HashSet
import qualified Data.Text as Text
import HeadedMegaparsec hiding (string)
import PostgresqlSyntax.Algebra hiding (parse, parseWithPosError, toText)
import PostgresqlSyntax.Extras.HeadedMegaparsec hiding (run)
import qualified PostgresqlSyntax.KeywordSet as KeywordSet
import qualified PostgresqlSyntax.Predicate as Predicate
import PostgresqlSyntax.Prelude hiding (bit, expr, filter, fromList, head, many, option, some, sortBy, tail, try)
import PostgresqlSyntax.Settings (Settings)
import qualified Text.Megaparsec as Megaparsec
import qualified Text.Megaparsec.Char as MegaparsecChar
import qualified TextBuilder
-- $setup
-- >>> import qualified PostgresqlSyntax.Extras.HeadedMegaparsec as HeadedMegaparsec
-- >>> testParser p = either putStr print . HeadedMegaparsec.run p
inSpace :: Parser a -> Parser a
inSpace p = space *> p <* space
commaSeparator :: Parser ()
commaSeparator = space *> char ',' *> endHead *> space
dotSeparator :: Parser ()
dotSeparator = space *> char '.' *> endHead *> space
inBrackets :: Parser a -> Parser a
inBrackets p = char '[' *> space *> p <* endHead <* space <* char ']'
inBracketsCont :: Parser a -> Parser (Parser a)
inBracketsCont p = char '[' *> endHead *> pure (space *> p <* endHead <* space <* char ']')
inParens :: Parser a -> Parser a
inParens p = char '(' *> space *> p <* endHead <* space <* char ')'
inParensCont :: Parser a -> Parser (Parser a)
inParensCont p = char '(' *> endHead *> pure (space *> p <* endHead <* space <* char ')')
inParensWithLabel :: (label -> content -> result) -> Parser label -> Parser content -> Parser result
inParensWithLabel result labelParser contentParser = do
label <- wrapToHead labelParser
space
char '('
endHead
space
content <- contentParser
space
char ')'
pure (result label content)
inParensWithClause :: Parser clause -> Parser content -> Parser content
inParensWithClause = inParensWithLabel (const id)
trueIfPresent :: Parser a -> Parser Bool
trueIfPresent p = option False (True <$ p)
-- |
-- >>> testParser (quotedString '\'') "'abc''d'"
-- "abc'd"
quotedString :: Char -> Parser Text
quotedString q = do
char q
endHead
tail <-
parse $
let collectChunks !bdr = do
chunk <- Megaparsec.takeWhileP Nothing (/= q)
let bdr' = bdr <> TextBuilder.text chunk
Megaparsec.try (consumeEscapedQuote bdr') <|> finish bdr'
consumeEscapedQuote bdr = do
MegaparsecChar.char q
MegaparsecChar.char q
collectChunks (bdr <> TextBuilder.char q)
finish bdr = do
MegaparsecChar.char q
return (TextBuilder.toText bdr)
in collectChunks mempty
return tail
-- |
-- >>> testParser dollarQuotedSconst "$$it's good$$"
-- "it's good"
dollarQuotedSconst :: Parser Text
dollarQuotedSconst = do
char '$'
quoteTag <- takeWhileP Nothing (/= '$')
let terminator = Megaparsec.chunk $ "$" <> quoteTag <> "$"
char '$'
endHead
tail <-
parse $ do
body <- many $ Megaparsec.notFollowedBy terminator *> Megaparsec.anySingle
terminator
return $ Text.pack body
return tail
-- * Keyword-matching infrastructure
--
-- Ported from "PostgresqlSyntax.Parsing". @keywordNameFromSet@\/@keywordNameByPredicate@
-- originally built an @Ident@ directly via its @UnquotedIdent@ constructor;
-- here that constructor is taken as an explicit argument (@wrap@) so this
-- module doesn't need to depend on the (still-monolithic-for-now)
-- 'PostgresqlSyntax.Ast.Ident' type. Callers pass @UnquotedIdent@ for it.
keywordNameFromSet wrap set = keywordNameByPredicate wrap (Predicate.inSet set)
keywordNameByPredicate wrap predicate =
fmap wrap $
filter
(\a -> "Reserved keyword " <> show a <> " used as an identifier. If that's what you intend, you have to wrap it in double quotes.")
predicate
anyKeyword
anyKeyword = parse $
Megaparsec.label "keyword" $
do
firstChar <- Megaparsec.satisfy Predicate.firstIdentifierChar
remainder <- Megaparsec.takeWhileP Nothing Predicate.notFirstIdentifierChar
let parsedKeyword = Text.toLower (Text.cons firstChar remainder)
when (parsedKeyword == "nulls") (Megaparsec.notFollowedBy nullsLa)
return parsedKeyword
where
nullsLa =
MegaparsecChar.space1
*> (MegaparsecChar.string' "first" <|> MegaparsecChar.string' "last")
*> Megaparsec.notFollowedBy (Megaparsec.satisfy Predicate.notFirstIdentifierChar)
-- | Expected keyword
--
-- Wraps the head in 'Megaparsec.region' to pin the reported error offset to
-- where this keyword check began. Without this, megaparsec >=9.8's stricter
-- (and correct) '(<|>)' error-merging (fix for
-- <https://github.com/mrkkrp/megaparsec/issues/412>) normalizes any error
-- whose offset lands past the enclosing alternative's start back down to
-- that start, discarding its \"expecting\" set unless the offset already
-- matches exactly. Since every failed keyword branch consumes some
-- identifier text before comparing, its offset always landed past the
-- alternative's start, so wide 'asum'/'choice' chains of 'keyword' lost
-- their combined expected-token sets under 9.8. Pinning the offset up front
-- keeps every branch's offset aligned with the alternative's start, so
-- their expected sets still union correctly.
keyword a = parse $ do
off <- Megaparsec.getOffset
Megaparsec.region (Megaparsec.setErrorOffset off) $ do
firstChar <- Megaparsec.satisfy Predicate.firstIdentifierChar
remainder <- Megaparsec.takeWhileP Nothing Predicate.notFirstIdentifierChar
let parsedKeyword = Text.toLower (Text.cons firstChar remainder)
if a == parsedKeyword then return parsedKeyword else empty
-- |
-- Consume a keyphrase, ignoring case and types of spaces between words.
keyphrase a =
Text.words a
& fmap (void . MegaparsecChar.string')
& intersperse MegaparsecChar.space1
& sequence_
& (<* Megaparsec.notFollowedBy (Megaparsec.satisfy Predicate.notFirstIdentifierChar))
& fmap (const (Text.toUpper a))
& Megaparsec.label (show a)
& parse
& (<* endHead)
-- * Cross-type-family helpers
--
-- Ported from "PostgresqlSyntax.Parsing". Each of these originally called a
-- single concrete node's parser directly (@typename@, @qualOp@,
-- @symbolicExprBinOp@, @fconst@\/@iconst@); here they're generalized over
-- 'IsAst' so any node module can instantiate them for its own node type(s)
-- without this module depending on any of them.
typecastExpr :: (IsAst typename) => Settings -> a -> (a -> typename -> a) -> Parser a
typecastExpr settings prefix constr = do
space
string "::"
endHead
space
type' <- parser settings
return (constr prefix type')
plusedExpr expr = char '+' *> space *> expr
minusedExpr expr = char '-' *> space *> expr
qualOpExpr :: (IsAst qualOp) => Settings -> Parser b -> (qualOp -> b -> a) -> Parser a
qualOpExpr settings expr constr = constr <$> wrapToHead (parser settings) <*> (space *> expr)
symbolicBinOpExpr :: (IsAst symbolicExprBinOp) => Settings -> a -> Parser b -> (a -> symbolicExprBinOp -> b -> c) -> Parser c
symbolicBinOpExpr settings a bParser constr = do
binOp <- label "binary operator" (space *> wrapToHead (parser settings) <* space)
b <- bParser
return (constr a binOp b)
iconstOrFconst :: (IsAst iconst, IsAst fconst) => Settings -> Parser (Either iconst fconst)
iconstOrFconst settings = Right <$> parser settings <|> Left <$> parser settings
-- |
-- Shared by 'PostgresqlSyntax.Ast.FuncApplicationParams' and
-- 'PostgresqlSyntax.Ast.SimpleSelect' (@ALL@\/@DISTINCT@ before a func-arg
-- list, and before a @UNION@\/@INTERSECT@\/@EXCEPT@ right-hand side,
-- respectively).
allOrDistinct :: Parser Bool
allOrDistinct = keyword "all" $> False <|> keyword "distinct" $> True
-- |
-- A ColId-like identifier parser (unreserved keyword ∪ col-name keyword)
-- restricted to exclude the given reserved words — needed wherever a
-- trailing bare word must terminate a construct instead of being consumed
-- as an identifier. Its only caller is
-- 'PostgresqlSyntax.Ast.RelationExprOptAlias', which excludes @SET@ only in
-- its bare (non-@AS@) alias branch, per @gram.y@'s
-- @relation_expr_opt_alias@ shift/reduce resolution. @identParser@ is the
-- identifier type's own plain (unfiltered) parser, tried first, same as
-- plain @ColId@ does.
filteredColIdLike :: (Text -> a) -> Parser a -> [Text] -> Parser a
filteredColIdLike wrap identParser excluded =
label "identifier" $
identParser
<|> keywordNameFromSet wrap (foldr HashSet.delete (KeywordSet.unreservedKeyword <> KeywordSet.colNameKeyword) excluded)