packages feed

postgresql-syntax-0.5.0.0: library-internal/PostgresqlSyntax/Ast/SimpleSelect.hs

module PostgresqlSyntax.Ast.SimpleSelect
  ( SimpleSelect (..),
    baseSimpleSelect,
    selectClauseBase,
    extendSelectClause,
  )
where

import qualified HeadedMegaparsec as Parser
import PostgresqlSyntax.Ast.FromClause
import PostgresqlSyntax.Ast.GroupClause
import PostgresqlSyntax.Ast.HavingClause
import PostgresqlSyntax.Ast.IntoClause
import PostgresqlSyntax.Ast.RelationExpr
import PostgresqlSyntax.Ast.SelectBinOp
import PostgresqlSyntax.Ast.SelectClause
import PostgresqlSyntax.Ast.Targeting
import PostgresqlSyntax.Ast.ValuesClause
import PostgresqlSyntax.Ast.WhereClause
import PostgresqlSyntax.Ast.WindowClause
import qualified PostgresqlSyntax.Helpers.Gens as Gens
import qualified PostgresqlSyntax.Helpers.Parsers as Parsers
import qualified PostgresqlSyntax.Helpers.TextBuilders as TextBuilders
import PostgresqlSyntax.IsAst
import PostgresqlSyntax.Prelude
import PostgresqlSyntax.Settings (Settings)
import qualified Test.QuickCheck as Qc
import qualified Text.Megaparsec as Megaparsec

-- |
-- ==== References
-- @
-- simple_select:
--   |  SELECT opt_all_clause opt_target_list
--       into_clause from_clause where_clause
--       group_clause having_clause window_clause
--   |  SELECT distinct_clause target_list
--       into_clause from_clause where_clause
--       group_clause having_clause window_clause
--   |  values_clause
--   |  TABLE relation_expr
--   |  select_clause UNION all_or_distinct select_clause
--   |  select_clause INTERSECT all_or_distinct select_clause
--   |  select_clause EXCEPT all_or_distinct select_clause
-- @
--
-- Hosts the real @select_clause@ grammar (including its
-- @UNION@\/@INTERSECT@\/@EXCEPT@-chaining) for both itself and
-- "PostgresqlSyntax.Ast.SelectNoParens", which shares it — see
-- 'PostgresqlSyntax.Ast.SelectClause'\'s module documentation for why.
data SimpleSelect
  = NormalSimpleSelect (Maybe Targeting) (Maybe IntoClause) (Maybe FromClause) (Maybe WhereClause) (Maybe GroupClause) (Maybe HavingClause) (Maybe WindowClause)
  | ValuesSimpleSelect ValuesClause
  | TableSimpleSelect RelationExpr
  | BinSimpleSelect SelectBinOp SelectClause (Maybe Bool) SelectClause
  deriving (Show, Generic, Eq, Ord, Data)

instance IsAst SimpleSelect where
  toTextBuilder settings = \case
    NormalSimpleSelect a b c d e f g ->
      TextBuilders.optLexemes
        [ Just "SELECT",
          fmap (toTextBuilder settings) a,
          fmap (toTextBuilder settings) b,
          fmap (toTextBuilder settings) c,
          fmap (toTextBuilder settings) d,
          fmap (toTextBuilder settings) e,
          fmap (toTextBuilder settings) f,
          fmap (toTextBuilder settings) g
        ]
    ValuesSimpleSelect a -> toTextBuilder settings a
    TableSimpleSelect a -> "TABLE " <> toTextBuilder settings a
    BinSimpleSelect a b c d -> toTextBuilder settings b <> " " <> toTextBuilder settings a <> foldMap (mappend " " . TextBuilders.renderAllOrDistinct) c <> " " <> toTextBuilder settings d
  parser settings = do
    a <- baseSimpleSelect settings <|> Parser.parse (Megaparsec.try (Parser.toParsec withParensHead))
    extendMany suffix a
    where
      suffix headSimpleSelect = binopExtension (SimpleSelectSelectClause headSimpleSelect)
      withParensHead = do
        swp <- parser settings
        binopExtension (WithParensSelectClause swp)
      binopExtension headClause = do
        op <- Parsers.space1 *> parser settings <* Parsers.space1
        Parser.endHead
        distinct <- optional (Parsers.allOrDistinct <* Parsers.space1)
        rhs <- selectClauseBase settings >>= extendSelectClause settings
        return (BinSimpleSelect op headClause distinct rhs)

-- |
-- The non-recursive base cases only (no @select_clause BINOP
-- select_clause@ extension) — see this module's own boot-exposed
-- signature.
baseSimpleSelect :: Settings -> Parser SimpleSelect
baseSimpleSelect settings =
  asum
    [ do
        Parsers.keyword "select"
        Parsers.notFollowedBy $ Parsers.satisfy isAlphaNum
        Parser.endHead
        targeting <- optional (Parsers.space1 *> parser settings)
        intoClause <- optional (Parsers.space1 *> parser settings)
        fromClause <- optional (Parsers.space1 *> parser settings)
        whereClause <- optional (Parsers.space1 *> parser settings)
        groupClause <- optional (Parsers.space1 *> parser settings)
        havingClause <- optional (Parsers.space1 *> parser settings)
        windowClause <- optional (Parsers.space1 *> parser settings)
        return (NormalSimpleSelect targeting intoClause fromClause whereClause groupClause havingClause windowClause),
      do
        Parsers.keyword "table"
        Parsers.space1
        Parser.endHead
        TableSimpleSelect <$> parser settings,
      ValuesSimpleSelect <$> parser settings
    ]

selectClauseBase :: Settings -> Parser SelectClause
selectClauseBase settings =
  asum
    [ WithParensSelectClause <$> parser settings,
      SimpleSelectSelectClause <$> baseSimpleSelect settings
    ]

extendSelectClause :: Settings -> SelectClause -> Parser SelectClause
extendSelectClause settings = extendMany suffix
  where
    suffix headSelectClause = SimpleSelectSelectClause <$> extensionSimpleSelect headSelectClause
    extensionSimpleSelect headSelectClause = do
      op <- Parsers.space1 *> parser settings <* Parsers.space1
      Parser.endHead
      distinct <- optional (Parsers.allOrDistinct <* Parsers.space1)
      rhs <- selectClauseBase settings >>= extendSelectClause settings
      return (BinSimpleSelect op headSelectClause distinct rhs)

instance Qc.Arbitrary SimpleSelect where
  shrink = fmap canonicalize . Qc.genericShrink
  arbitrary =
    canonicalize
      <$> Qc.sized
        ( \n ->
            if n <= 1
              then TableSimpleSelect <$> Qc.arbitrary
              else
                Qc.oneof
                  [ NormalSimpleSelect
                      <$> Qc.arbitrary
                      <*> Qc.arbitrary
                      <*> Qc.arbitrary
                      <*> Gens.terminatingMaybe Qc.arbitrary
                      <*> Qc.arbitrary
                      <*> Gens.terminatingMaybe Qc.arbitrary
                      <*> Qc.arbitrary,
                    ValuesSimpleSelect <$> Qc.arbitrary,
                    TableSimpleSelect <$> Qc.arbitrary,
                    BinSimpleSelect <$> Qc.arbitrary <*> Qc.arbitrary <*> Qc.arbitrary <*> Qc.arbitrary
                  ]
        )

-- |
-- Collapses a left-associated @BinSimpleSelect@ chain (@(a OP1 b) OP2
-- c@) to the right-associated shape (@a OP1 (b OP2 c)@) that the parser
-- actually produces: 'parser' above parses each operator's right-hand
-- side via 'extendSelectClause', which itself greedily consumes the rest
-- of the chain before returning — so a chain of @N@ operators nests
-- entirely to the right, and only that shape is reachable by parsing the
-- rendered text (both shapes render identically, since rendering doesn't
-- parenthesize chain elements). Both 'arbitrary' and 'shrink' can
-- otherwise construct the non-canonical shape, which renders fine but
-- parses back to a different, canonical value and so breaks the
-- roundtrip property.
canonicalize :: SimpleSelect -> SimpleSelect
canonicalize s@(BinSimpleSelect {}) =
  case rest of
    (op, distinct, next) : more -> BinSimpleSelect op headClause distinct (buildRight next more)
    [] -> s
  where
    (headClause, rest) = flattenChain (SimpleSelectSelectClause s)
    buildRight lastClause [] = lastClause
    buildRight clause ((op, distinct, next) : more) = SimpleSelectSelectClause (BinSimpleSelect op clause distinct (buildRight next more))
    flattenChain (SimpleSelectSelectClause (BinSimpleSelect op lhs distinct rhs)) =
      let (lhsHead, lhsRest) = flattenChain lhs
          (rhsHead, rhsRest) = flattenChain rhs
       in (lhsHead, lhsRest <> [(op, distinct, rhsHead)] <> rhsRest)
    flattenChain c = (c, [])
canonicalize other = other