packages feed

selda 0.1.10.1 → 0.1.11.0

raw patch · 11 files changed

+125/−49 lines, 11 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Database.Selda: distinct :: Query s a -> Query s a
+ Database.Selda: ifThenElse :: Col s Bool -> Col s a -> Col s a -> Col s a
+ Database.Selda: matchNull :: SqlType a => Col s b -> (Col s a -> Col s b) -> Col s (Maybe a) -> Col s b

Files

ChangeLog.md view
@@ -1,6 +1,13 @@ # Revision history for Selda  +## 0.1.11.0 -- 2017-09-08++* Fix name generation in the presence of isIn over queries.+* SELECT DISTINCT support.+* Conditional expressions and matchNull.++ ## 0.1.10.1 -- 2017-08-11  * Fix name generation in the presence of multiple aggregates.
selda.cabal view
@@ -1,5 +1,5 @@ name:                selda-version:             0.1.10.1+version:             0.1.11.0 synopsis:            Type-safe, high-level EDSL for interacting with relational databases. description:         This package provides an EDSL for writing portable, type-safe, high-level                      database code. Its feature set includes querying and modifying databases,
src/Database/Selda.hs view
@@ -73,7 +73,7 @@   , Cols, Columns   , Order (..)   , (:*:)(..)-  , select, selectValues, from+  , select, selectValues, from, distinct   , restrict, limit   , order , ascending, descending   , inner, suchThat@@ -83,7 +83,7 @@   , (.==), (./=), (.>), (.<), (.>=), (.<=), like   , (.&&), (.||), not_   , literal, int, float, text, true, false, null_-  , roundTo, length_, isNull+  , roundTo, length_, isNull, ifThenElse, matchNull     -- * Converting between column types   , round_, just, fromBool, fromInt, toString     -- * Inner queries@@ -132,7 +132,7 @@ import Database.Selda.Query import Database.Selda.Query.Type import Database.Selda.Selectors-import Database.Selda.SQL+import Database.Selda.SQL hiding (distinct) import Database.Selda.SqlType import Database.Selda.Table import Database.Selda.Table.Compile@@ -214,6 +214,13 @@ isNull :: Col s (Maybe a) -> Col s Bool isNull = liftC $ UnOp IsNull +-- | Applies the given function to the given nullable column where it isn't null,+--   and returns the given default value where it is.+--+--   This is the Selda equivalent of 'maybe'.+matchNull :: SqlType a => Col s b -> (Col s a -> Col s b) -> Col s (Maybe a) -> Col s b+matchNull nullvalue f x = ifThenElse (isNull x) nullvalue (f (cast x))+ -- | Any container type for which we can check object membership. class Set set where   -- | Is the given column contained in the given set?@@ -226,7 +233,7 @@   isIn (C x) xs = C $ InList x (unsafeCoerce xs)  instance Set (Query s) where-  isIn (C x) = C . InQuery x . snd . compQuery+  isIn (C x) = C . InQuery x . snd . compQueryWithFreshScope  (.&&), (.||) :: Col s Bool -> Col s Bool -> Col s Bool (.&&) = liftC2 $ BinOp And@@ -338,3 +345,7 @@ -- | Convert any column to a string. toString :: Col s a -> Col s Text toString = cast++-- | Perform a conditional on a column+ifThenElse :: Col s Bool -> Col s a -> Col s a -> Col s a+ifThenElse = liftC3 If
src/Database/Selda/Column.hs view
@@ -3,7 +3,7 @@ module Database.Selda.Column   ( Cols, Columns   , Col (..), SomeCol (..), Exp (..), UnOp (..), BinOp (..)-  , toTup, fromTup, liftC, liftC2+  , toTup, fromTup, liftC, liftC2, liftC3   , allNamesIn   , literal   ) where@@ -45,6 +45,9 @@  instance IsString (Col s Text) where   fromString = literal . fromString++liftC3 :: (Exp SQL a -> Exp SQL b -> Exp SQL c -> Exp SQL d) -> Col s a -> Col s b -> Col s c -> Col s d+liftC3 f (C a) (C b) (C c) = C (f a b c)  liftC2 :: (Exp SQL a -> Exp SQL b -> Exp SQL c) -> Col s a -> Col s b -> Col s c liftC2 f (C a) (C b) = C (f a b)
src/Database/Selda/Compile.hs view
@@ -3,7 +3,7 @@ -- | Selda SQL compilation. module Database.Selda.Compile   ( Result, Res-  , toRes, compQuery+  , toRes, compQuery, compQueryWithFreshScope   , compile, compileWith, compileWithTables   , compileInsert, compileUpdate, compileDelete   )@@ -22,6 +22,10 @@ import Data.Text (Text, empty) import Data.Typeable (Typeable) +-- For scope supply+import Data.IORef+import System.IO.Unsafe+ -- | Compile a query into a parameterised SQL statement. -- --   The types given are tailored for SQLite. To translate SQLite types into@@ -39,7 +43,7 @@                   => PPConfig                   -> Query s a                   -> ([TableName], (Text, [Param]))-compileWithTables cfg = compSql cfg . snd . compQuery+compileWithTables cfg = compSql cfg . snd . compQuery 0  -- | Compile an @INSERT@ query, given the keyword representing default values --   in the target SQL dialect, a table and a list of items corresponding@@ -83,15 +87,26 @@  -- | Compile a query to an SQL AST. --   Groups are ignored, as they are only used by 'aggregate'.-compQuery :: Result a => Query s a -> (Int, SQL)-compQuery q =-    (nameSupply st, SQL final (Product [srcs]) [] [] [] Nothing)+compQuery :: Result a => Scope -> Query s a -> (Int, SQL)+compQuery ns q =+    (nameSupply st, SQL final (Product [srcs]) [] [] [] Nothing False)   where-    (cs, st) = runQueryM q+    (cs, st) = runQueryM ns q     final = finalCols cs     sql = state2sql st     live = colNames final ++ allNonOutputColNames sql     srcs = removeDeadCols live sql++{-# NOINLINE scopeSupply #-}+scopeSupply :: IORef Scope+scopeSupply = unsafePerformIO $ newIORef 1++-- | Get a fresh scope from the global scope supply, then use it to compile+--   the given query.+compQueryWithFreshScope :: Result a => Query s a -> (Int, SQL)+compQueryWithFreshScope q = unsafePerformIO $ do+  s <- atomicModifyIORef' scopeSupply (\s -> (s+1, s))+  return $ compQuery s q  -- | An acceptable query result type; one or more columns stitched together --   with @:*:@.
src/Database/Selda/Exp.hs view
@@ -20,6 +20,7 @@   BinOp   :: !(BinOp a b) -> !(Exp sql a) -> !(Exp sql a) -> Exp sql b   UnOp    :: !(UnOp a b) -> !(Exp sql a) -> Exp sql b   Fun2    :: !Text -> !(Exp sql a) -> !(Exp sql b) -> Exp sql c+  If      :: !(Exp sql Bool) -> !(Exp sql a) -> !(Exp sql a) -> Exp sql a   Cast    :: !SqlTypeRep -> !(Exp sql a) -> Exp sql b   AggrEx  :: !Text -> !(Exp sql a) -> Exp sql b   InList  :: !(Exp sql a) -> ![Exp sql a] -> Exp sql Bool@@ -63,6 +64,7 @@   allNamesIn (BinOp _ a b) = allNamesIn a ++ allNamesIn b   allNamesIn (UnOp _ a)    = allNamesIn a   allNamesIn (Fun2 _ a b)  = allNamesIn a ++ allNamesIn b+  allNamesIn (If a b c)    = allNamesIn a ++ allNamesIn b ++ allNamesIn c   allNamesIn (Cast _ x)    = allNamesIn x   allNamesIn (AggrEx _ x)  = allNamesIn x   allNamesIn (InList x xs) = concatMap allNamesIn (x:xs)
src/Database/Selda/Query.hs view
@@ -1,14 +1,15 @@ {-# LANGUAGE FlexibleContexts, OverloadedStrings #-} -- | Query monad and primitive operations. module Database.Selda.Query-  ( select, selectValues+  ( select, selectValues, Database.Selda.Query.distinct   , restrict, groupBy, limit, order   , aggregate, leftJoin, innerJoin   ) where+import Data.Maybe (isNothing) import Database.Selda.Column import Database.Selda.Inner import Database.Selda.Query.Type-import Database.Selda.SQL+import Database.Selda.SQL as SQL import Database.Selda.Table import Database.Selda.Transform import Control.Monad.State.Strict@@ -20,7 +21,7 @@ select (Table name cs _) = Query $ do     rns <- mapM (rename . Some . Col) cs'     st <- get-    put $ st {sources = SQL rns (TableName name) [] [] [] Nothing : sources st}+    put $ st {sources = sqlFrom rns (TableName name) : sources st}     return $ toTup [n | Named n _ <- rns]   where     cs' = map colName cs@@ -30,14 +31,14 @@ selectValues :: (Insert a, Columns (Cols s a)) => [a] -> Query s (Cols s a) selectValues [] = Query $ do   st <- get-  put $ st {sources = SQL [] EmptyTable [] [] [] Nothing : sources st}+  put $ st {sources = sqlFrom [] EmptyTable : sources st}   return $ toTup (repeat "NULL") selectValues (row:rows) = Query $ do     names <- mapM (const freshName) firstrow     let rns = [Named n (Col n) | n <- names]         row' = mkFirstRow names     s <- get-    put $ s {sources = SQL rns (Values row' rows') [] [] [] Nothing : sources s}+    put $ s {sources = sqlFrom rns (Values row' rows') : sources s}     return $ toTup [n | Named n _ <- rns]   where     firstrow = map defToVal $ params row@@ -60,10 +61,10 @@       -- of the query where they are renamed, so if the restrict predicate       -- contains any vars renamed in this query, we must add another query       -- just for the restrict.-      [SQL cs s ps gs os lim] | not $ p `wasRenamedIn` cs ->-        st {sources = [SQL cs s (p : ps) gs os lim]}+      [sql] | not $ p `wasRenamedIn` cols sql ->+        st {sources = [sql {restricts = p : restricts sql}]}       ss ->-        st {sources = [SQL (allCols ss) (Product ss) [p] [] [] Nothing]}+        st {sources = [(sqlFrom (allCols ss) (Product ss)) {restricts = [p]}]}   where     wasRenamedIn predicate cs =       let cs' = [n | Named n _ <- cs]@@ -98,9 +99,8 @@ aggregate q = Query $ do   (gst, aggrs) <- isolate q   cs <- mapM rename $ unAggrs aggrs-  let sql = state2sql gst-      sql' = SQL cs (Product [sql]) [] (groupCols gst) [] Nothing-  modify $ \st -> st {sources = sql' : sources st}+  let sql = (sqlFrom cs (Product [state2sql gst])) {groups = groupCols gst}+  modify $ \st -> st {sources = sql : sources st}   pure $ toTup [n | Named n _ <- cs]  -- | Perform a @LEFT JOIN@ with the current result set (i.e. the outer query)@@ -151,11 +151,10 @@   st <- get   let nameds = [n | Named n _ <- cs]       left = state2sql st-      right = SQL cs (Product [state2sql join_st]) [] [] [] Nothing+      right = sqlFrom cs (Product [state2sql join_st])       C on = check $ toTup nameds       outCols = [Some $ Col n | Named n _ <- cs] ++ allCols [left]-      sql = SQL outCols (Join jointype on left right) [] [] [] Nothing-  put $ st {sources = [sql]}+  put $ st {sources = [sqlFrom outCols (Join jointype on left right)]}   pure $ toTup nameds  -- | Group an aggregate query by a column.@@ -180,12 +179,10 @@ limit from to q = Query $ do   (lim_st, res) <- isolate q   st <- get-  let sql = case sources lim_st of-        [SQL cs s ps gs os Nothing] ->-          SQL cs s ps gs os (Just (from, to))-        ss ->-          SQL (allCols ss) (Product ss) [] [] [] (Just (from, to))-  put $ st {sources = sql : sources st}+  let sql' = case sources lim_st of+        [sql] | isNothing (limits sql) -> sql+        ss                             -> sqlFrom (allCols ss) (Product ss)+  put $ st {sources = sql' {limits = Just (from, to)} : sources st}   -- TODO: replace with safe coercion   return $ unsafeCoerce res @@ -213,8 +210,17 @@ order :: Col s a -> Order -> Query s () order (C c) o = Query $ do   st <- get-  put $ case sources st of-    [SQL cs s ps gs os lim] ->-      st {sources = [SQL cs s ps gs ((o, Some c):os) lim]}-    ss ->-      st {sources = [SQL (allCols ss) (Product ss) [] [] [(o, Some c)] Nothing]}+  case sources st of+    [sql] -> put st {sources = [sql {ordering = (o, Some c) : ordering sql}]}+    ss    -> put st {sources = [sql {ordering = [(o,Some c)]}]}+      where sql = sqlFrom (allCols ss) (Product ss)++-- | Remove all duplicates from the result set.+distinct :: Query s a -> Query s a+distinct q = Query $ do+  (inner_st, res) <- isolate q+  st <- get+  case sources inner_st of+    [sql] -> put st {sources = [sql {SQL.distinct = True}]}+    ss    -> put st {sources = [sqlFrom (allCols ss) (Product ss)]}+  return res
src/Database/Selda/Query/Type.hs view
@@ -7,19 +7,29 @@ import Database.Selda.Column import Database.Selda.Types (ColName, mkColName, addColSuffix) +type Scope = Int+type Ident = Int++-- | A name, consisting of a scope and an identifier.+data Name = Name Scope Ident++instance Show Name where+  show (Name 0 n) = concat [show n]+  show (Name s n) = concat [show s, "s_", show n]+ -- | An SQL query. newtype Query s a = Query {unQ :: State GenState a}   deriving (Functor, Applicative, Monad)  -- | Run a query computation from an initial state.-runQueryM :: Query s a -> (a, GenState)-runQueryM = flip runState initState . unQ+runQueryM :: Scope -> Query s a -> (a, GenState)+runQueryM scope = flip runState (initState scope) . unQ  -- | Run a query computation in isolation, but reusing the current name supply. isolate :: Query s a -> State GenState (GenState, a) isolate (Query q) = do   st <- get-  put $ initState {nameSupply = nameSupply st}+  put $ (initState (nameScope st)) {nameSupply = nameSupply st}   x <- q   st' <- get   put $ st {nameSupply = nameSupply st'}@@ -34,15 +44,17 @@   , staticRestricts :: ![Exp SQL Bool]   , groupCols       :: ![SomeCol SQL]   , nameSupply      :: !Int+  , nameScope       :: !Int   }  -- | Initial state: no subqueries, no restrictions.-initState :: GenState-initState = GenState+initState :: Int -> GenState+initState scope = GenState   { sources = []   , staticRestricts = []   , groupCols = []   , nameSupply = 0+  , nameScope  = scope   }  -- | Generate a unique name for the given column.@@ -59,11 +71,11 @@   return col  -- | Get a guaranteed unique identifier.-freshId :: State GenState Int+freshId :: State GenState Name freshId = do   st <- get   put $ st {nameSupply = succ $ nameSupply st}-  return (nameSupply st)+  return (Name (nameScope st) (nameSupply st))  -- | Get a guaranteed unique column name. freshName :: State GenState ColName
src/Database/Selda/SQL.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, RecordWildCards #-} {-# LANGUAGE TypeOperators, FlexibleInstances, UndecidableInstances #-}+{-# LANGUAGE RankNTypes #-} -- | SQL AST and parameters for prepared statements. module Database.Selda.SQL where import Database.Selda.Exp@@ -28,6 +29,7 @@   , groups    :: ![SomeCol SQL]   , ordering  :: ![(Order, SomeCol SQL)]   , limits    :: !(Maybe (Int, Int))+  , distinct  :: !Bool   }  instance Names SqlSource where@@ -46,6 +48,19 @@     , allNamesIn restricts     , allNamesIn source     ]++-- | Build a plain SQL query with the given columns and source, with no filters,+--   ordering, etc.+sqlFrom :: [SomeCol SQL] -> SqlSource -> SQL+sqlFrom cs src = SQL+  { cols = cs+  , source = src+  , restricts = []+  , groups = []+  , ordering = []+  , limits = Nothing+  , distinct = False+  }  -- | The order in which to sort result rows. data Order = Asc | Desc
src/Database/Selda/SQL/Print.hs view
@@ -109,7 +109,7 @@  -- | Pretty-print an SQL AST. ppSql :: SQL -> PP Text-ppSql (SQL cs src r gs ord lim) = do+ppSql (SQL cs src r gs ord lim dist) = do   cs' <- mapM ppSomeCol cs   src' <- ppSrc src   r' <- ppRestricts r@@ -117,7 +117,7 @@   ord' <- ppOrder ord   lim' <- ppLimit lim   pure $ mconcat-    [ "SELECT ", result cs'+    [ "SELECT ", if dist then "DISTINCT " else "", result cs'     , src'     , r'     , gs'@@ -220,6 +220,11 @@   a' <- ppCol a   b' <- ppCol b   pure $ mconcat [f, "(", a', ", ", b', ")"]+ppCol (If a b c)     = do+  a' <- ppCol a+  b' <- ppCol b+  c' <- ppCol c+  pure $ mconcat ["CASE WHEN ", a', " THEN ", b', " ELSE ", c', " END"] ppCol (AggrEx f x)   = ppUnOp (Fun f) x ppCol (Cast t x)     = do   x' <- ppCol x
src/Database/Selda/Transform.hs view
@@ -65,10 +65,10 @@ -- | Build the outermost query from the SQL generation state. --   Groups are ignored, as they are only used by 'aggregate'. state2sql :: GenState -> SQL-state2sql (GenState [sql] srs _ _) =+state2sql (GenState [sql] srs _ _ _) =   sql {restricts = restricts sql ++ srs}-state2sql (GenState ss srs _ _) =-  SQL (allCols ss) (Product ss) srs [] [] Nothing+state2sql (GenState ss srs _ _ _) =+  SQL (allCols ss) (Product ss) srs [] [] Nothing False  -- | Get all output columns from a list of SQL ASTs. allCols :: [SQL] -> [SomeCol SQL]