packages feed

preql-0.4: test/Test/Syntax/Generators.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
module Test.Syntax.Generators where

import Preql.QuasiQuoter.Syntax.Name
import Preql.QuasiQuoter.Syntax.Syntax as Syntax

import Control.Applicative
import Control.Monad
import Data.Maybe (isNothing)
import Data.Set (Set)
import Data.Text (Text)
import Hedgehog
import Hedgehog.Internal.Range (clamp)
import qualified Data.Char as Char
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

lit :: Gen Literal
lit = Gen.choice
  -- positive numeric literals only, use Unary Negate for negative
  [ I <$> Gen.sized \case
      -- randomly pick [1,10] when we get down to size 1
      1 -> Gen.prune (Gen.integral (Range.linear 1 10))
      _ -> Gen.integral (Range.linearFrom 1 0 maxBound)
  , F <$> Gen.double (Range.linearFracFrom 1 0 1e300)
  , T <$> Gen.text (Range.linear 0 100) unicodeNotControl
  , B <$> Gen.bool
  , pure Null
  ]

litE :: Gen Expr
litE = Lit <$> lit

unicodeNotControl :: Gen Char
unicodeNotControl = Gen.filter (\c -> Char.ord c > 31 && c /= '\DEL' && c /= '\'' ) Gen.unicode

-- TODO Gen.prune once we get down to length 1
name_ :: Gen Name
name_ = Gen.sized \case
  -- don't shrink to "a"; assume all letters are the same, make queries more legible
  1 -> Gen.prune (Name <$> (T.cons <$> Gen.lower <*> pure ""))
  _ -> Name <$> Gen.filter (flip Set.notMember keywords)
    (T.cons <$> Gen.lower <*>
     Gen.text (Range.linear 0 29) (Gen.frequency [(26, Gen.lower), (1, pure '_')]))

haskellVarName :: Gen Text
haskellVarName = T.cons <$> Gen.lower <*> Gen.text (Range.linear 0 29) Gen.alphaNum


select :: Gen SelectStmt
select = select_

select_ :: Gen SelectStmt
select_ = Gen.sized \case
  1 -> Gen.frequency smallSelects
  _ -> Gen.frequency ( smallSelects ++ [ (20, setSelect) ])
 where
  smallSelects =
    [ (40, Simple <$> simpleSelect)
    , (20, SelectValues <$> Gen.nonEmpty (Range.exponential 1 100)
            (Gen.nonEmpty (Range.exponential 1 20) (scaleOne valueExpr)))
    , (20, S <$> selectWithoutOptions <*> selectOptions_)
    ]
  setSelect = Set <$> Gen.enumBounded <*> Gen.enumBounded <*> scaleHalf select_ <*> scaleHalf select_
  selectWithoutOptions = Gen.filter noOptions (scaleOne select_)
  noOptions (S _ _) = False
  noOptions _ = True

scaledList :: Range Int -> Gen a -> Gen [a]
scaledList range ga = do
  n <- Gen.integral range
  let scale = Gen.scale (clampSize . (`div` Size n))
  Gen.list (Range.singleton n) (scale ga)

simpleSelect :: Gen Select
simpleSelect = do
  -- now bind fields of Syntax.Select
  distinct <- Gen.maybe distinctClause
  -- TODO why don't we support table.* in ResTarget?  Is it part of a_expr?
  targetList <- Gen.frequency [(1, pure [Star]), (99, scaledList (Range.linear 1 15) columnTarget)]
  from <- scaledList (Range.linear 1 10) tableRef
  whereClause <- Gen.maybe (scaleHalf expr)
  groupBy <- scaledList (Range.linear 0 5) expr
  having <- Gen.maybe (scaleHalf expr)
  return $ Syntax.select {distinct, from, targetList, whereClause, groupBy, having}

selectOptions_ :: Gen SelectOptions
selectOptions_ = Gen.filter nonTrivial do
  sortBy <- Gen.list (Range.linear 0 5) sortBy_
  offset <- Gen.maybe (scaleOne expr)
  limit <- Gen.maybe (scaleOne expr)
  locking <- scaledList (Range.linear 0 3) locking_
  let withClause = Nothing
  return SelectOptions{..}
 where
   nonTrivial SelectOptions{..} =
     not (null sortBy && isNothing offset && isNothing limit && null locking && isNothing withClause)

valueExpr :: Gen Expr
valueExpr = Gen.filter notSelectExpr expr where
  notSelectExpr (SelectExpr _) = False
  notSelectExpr _ = True

expr :: Gen Expr
expr = Gen.sized \case
  -- TODO frequency
  0 -> Gen.choice zeros
  1 -> Gen.choice (zeros ++ ones)
  _ -> Gen.choice (zeros ++ ones ++ twos)
 where
  zeros =
    [ litE
    , CRef <$> name_
    , NumberedParam <$> Gen.integral (Range.linear 1 20)
    , HaskellParam <$> haskellVarName
    ]
  ones =
    [ Unary <$> unaryOp <*> scaleOne expr
    , L <$> scaleOne likeE
    , Indirection <$> scaleOne expr <*> Gen.nonEmpty (Range.linear 1 4) name_
    ]
  twos =
    [ BinOp <$> binOp <*> scaleHalf expr <*> scaleHalf expr
    -- One might expect scaleOne for SelectExpr, but in practice that
    -- leads to unacceptably long test times for Expr.  It might be
    -- nice to work out the maximum number of constructors (or p90?)
    -- in SelectStmt of size n, and do something more precise here.
    , SelectExpr <$> scaleHalf select_
    , Fun <$> funApp
    , Cas <$> caseE
    ]

unaryOp :: Gen UnaryOp
unaryOp = Gen.enumBounded

binOp :: Gen BinOp
binOp = Gen.enumBounded

mathOp :: Gen BinOp
mathOp = Gen.element [ Mul, Div, Add, Sub, Exponent, Mod, Eq, Syntax.LT, LTE, Syntax.GT, GTE, NEq ]

likeOp :: Gen LikeOp
likeOp = Gen.enumBounded

likeE :: Gen LikeE
likeE = do
  op <- likeOp
  string <- scaleHalf expr
  likePattern <- scaleHalf expr
  escape <- Gen.frequency
    [(50, pure Nothing), (49, Just . Lit . T . T.singleton <$> Gen.alphaNum), (1, Just <$> scaleHalf expr)]
  invert <- Gen.bool
  return LikeE {..}

funApp :: Gen FunctionApplication
funApp = do
  name <- name_
  indirection <- Gen.list (Range.linear 0 3) name_
  len_arguments <- Gen.integral (Range.linear (-1) 10)
  len_sortBy <- if len_arguments > 0 then pure 0 else Gen.integral (Range.linear 0 3)
  len_withinGroup <- if len_sortBy == 0 then pure 0 else Gen.integral (Range.linear 0 3)
  hasFilter <- Gen.bool
  hasOver <- Gen.bool
  let
    n = len_sortBy + len_withinGroup + min 0 len_arguments + if hasFilter then 1 else 0 + if hasOver then 3 else 0
    scale = if n <= 0 then id else Gen.scale (clampSize . (`div` Size n))
  arguments <- case len_arguments of
    -1 -> pure StarArg
    0 -> pure NoArgs
    n -> do
      arguments <- Gen.nonEmpty (Range.singleton len_arguments)
        (Gen.choice [ E <$> scale expr, Named <$> name_ <*> scale expr ])
      sortBy <- Gen.list (Range.singleton len_sortBy) (scale sortBy_)
      distinct <- Gen.bool
      return $ Args ArgsList {..}
  withinGroup <- Gen.list (Range.singleton len_withinGroup) (scale sortBy_)
  filterClause <- if hasFilter then Just <$> scale expr  else pure Nothing
  over <- if hasOver then scale over_ else pure noWindow
  return FApp{..}

caseE :: Gen Case
caseE = do
  hasImplicit <- Gen.bool
  hasElse <- Gen.bool
  whenSize <- Gen.integral (Range.linear 1 10)
  let
    oneIf x = if x then 1 else 0
    n = 2 * whenSize + oneIf hasImplicit + oneIf hasElse
    scale = Gen.scale (clampSize . (`div` n))
    justIf x = if x then Just <$> scale expr else pure Nothing
  whenClause <- replicateM (Range.unSize whenSize) (liftA2 (,) (scale expr) (scale expr))
  implicitArg <- justIf hasImplicit
  elseClause <- justIf hasElse
  return Case{..}

tableRef :: Gen TableRef
tableRef = Gen.sized \case
    0 -> singleTable
    1 -> Gen.choice [ singleTable, aliased ]
    _ -> Gen.choice [ singleTable, aliased, subSelect ]
  where
    singleTable = J <$> joinedTable
    aliased = As <$> scaleOne joinedTable <*> alias
    alias = Alias <$> name_ <*>
      Gen.choice [pure [], Gen.list (Range.linear 1 5) name_]
    subSelect = SubSelect <$> scaleOne select_ <*> alias

joinedTable :: Gen JoinedTable
joinedTable = Gen.sized \case
    0 -> singleTable
    1 -> singleTable
    _ -> Gen.choice [ singleTable, joined, crossJoin ]
  where
    singleTable = Table <$> name_
    joined = Join <$> Gen.enumBounded <*> joinQual
             <*> scaleHalf tableRef <*> scaleHalf tableRef
    crossJoin = CrossJoin <$> scaleHalf tableRef <*> scaleHalf tableRef

joinQual :: Gen JoinQual
joinQual = Gen.choice
  [ pure Natural
  , Using <$> Gen.list (Range.linear 1 10) name_
  , On <$> scaleOne expr
  ]

-- | 'Star' is generated in 'select', so we only do @Column@ here
columnTarget :: Gen ResTarget
columnTarget = Column <$> valueExpr <*> Gen.maybe name_

distinctClause :: Gen DistinctClause
distinctClause = Gen.frequency
  [ (1, pure DistinctAll)
  , (9, DistinctOn <$> Gen.nonEmpty (Range.linear 1 5) expr) ]

over_ :: Gen Over
over_ = do
  Gen.choice [ WindowName <$> name_ , Window <$> window_ ]

window_ :: Gen WindowSpec
window_ = do
  refName <- Gen.maybe name_
  partitionClause <- Gen.list (Range.linear 0 3) expr
  orderClause <- Gen.list (Range.linear 0 3) sortBy_
  return WindowSpec {..}

sortBy_ :: Gen SortBy
sortBy_ = SortBy <$> expr <*> sortOrderOrUsing <*> nullsOrder

sortOrderOrUsing :: Gen SortOrderOrUsing
sortOrderOrUsing = Gen.choice [ SortOrder <$> sortOrder, SortUsing <$> mathOp ]

sortOrder :: Gen SortOrder
sortOrder = Gen.enumBounded

nullsOrder :: Gen NullsOrder
nullsOrder = Gen.enumBounded

locking_ :: Gen Locking
locking_ = do
  strength <- Gen.enumBounded
  tables <- Gen.list (Range.linear 0 5) name_
  wait <- Gen.enumBounded
  return Locking{..}

clampSize :: Size -> Size
clampSize = clamp 0 99

scaleOne :: MonadGen m => m a -> m a
scaleOne = Gen.scale (\s -> clampSize (s - 1))

scaleHalf :: MonadGen m => m a -> m a
scaleHalf = Gen.scale (clampSize . (`div` 2))

keywords :: Set Text
keywords = Set.union reserved_keywords type_func_name_keyword

-- Reserved keyword - these keywords are usable only as a ColLabel.
reserved_keywords :: Set Text
reserved_keywords = Set.fromList
  [ "all", "analyse", "analyze", "and", "any", "array", "as", "asc", "asymmetric", "both", "case", "cast", "check", "collate", "column", "constraint", "create", "current_catalog", "current_date", "current_role", "current_time", "current_timestamp", "current_user", "default", "deferrable", "desc", "distinct", "do", "else", "end", "except", "false", "fetch", "for", "foreign", "from", "grant", "group", "having", "in", "initially", "intersect", "into", "lateral", "leading", "limit", "localtime", "localtimestamp", "not", "null", "offset", "on", "only", "or", "order", "placing", "primary", "references", "returning", "select", "current_user", "some", "symmetric", "table", "then", "to", "trailing", "true", "union", "unique", "user", "using", "variadic", "when", "where", "window", "with" ]

-- Type/function identifier - keywords that can be type or function names.
type_func_name_keyword :: Set Text
type_func_name_keyword = Set.fromList
  [ "authorization", "binary", "collation", "concurrently", "cross", "current_schema", "freeze", "full", "ilike", "inner", "is", "isnull", "join", "left", "like", "natural", "notnull", "outer", "overlaps", "right", "similar", "tablesample", "verbose" ]