packages feed

selda 0.5.0.0 → 0.5.1.0

raw patch · 10 files changed

+133/−16 lines, 10 filesdep ~randomdep ~timePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: random, time

API changes (from Hackage documentation)

+ Database.Selda: UnsafeError :: String -> SeldaError
+ Database.Selda.Backend: UnsafeError :: String -> SeldaError
+ Database.Selda.Backend.Internal: UnsafeError :: String -> SeldaError
+ Database.Selda.Unsafe: data QueryFragment
+ Database.Selda.Unsafe: inj :: Col s a -> QueryFragment
+ Database.Selda.Unsafe: injLit :: SqlType a => a -> QueryFragment
+ Database.Selda.Unsafe: rawExp :: SqlType a => Text -> Col s a
+ Database.Selda.Unsafe: rawName :: SqlType a => ColName -> Col s a
+ Database.Selda.Unsafe: rawQuery :: forall a s. SqlRow a => [ColName] -> QueryFragment -> Query s (Row s a)
+ Database.Selda.Unsafe: rawQuery1 :: SqlType a => ColName -> QueryFragment -> Query s (Col s a)
+ Database.Selda.Unsafe: rawStm :: MonadSelda m => QueryFragment -> m ()

Files

ChangeLog.md view
@@ -1,6 +1,14 @@ # Revision history for Selda  +## 0.5.1.0 -- 2020-01-20++* Support for raw SQL fragments. (#134)+* Expose tableName.+* Document performance drawbacks of withoutForeignKeyEnforcement.+* Fix several bugs validating auto-incrementing PKs. (#133)++ ## 0.5.0.0 -- 2019-09-21  * index and indexUsing now accept a Group instead of a Selector (#121)
selda.cabal view
@@ -1,5 +1,5 @@ name:                selda-version:             0.5.0.0+version:             0.5.1.0 synopsis:            Multi-backend, high-level EDSL for interacting with SQL 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
@@ -25,7 +25,7 @@   , SeldaError (..), ValidationError   , SeldaT, SeldaM   , Relational, Only (..), The (..)-  , Table, Query, Row, Col, Res, Result+  , Table (tableName), Query, Row, Col, Res, Result   , query, queryInto   , transaction, withoutForeignKeyEnforcement   , newUuid
src/Database/Selda/Backend/Internal.hs view
@@ -17,6 +17,7 @@   , newConnection, allStmts   , runSeldaT, withBackend   ) where+import Data.List (nub) import Database.Selda.SQL (Param (..)) import Database.Selda.SqlType import Database.Selda.Table hiding (colName, colType, colFKs)@@ -45,8 +46,10 @@  -- | Thrown by any function in 'SeldaT' if an error occurs. data SeldaError-  = DbError String  -- ^ Unable to open or connect to database.-  | SqlError String -- ^ An error occurred while executing query.+  = DbError String     -- ^ Unable to open or connect to database.+  | SqlError String    -- ^ An error occurred while executing query.+  | UnsafeError String -- ^ An error occurred due to improper use of an unsafe+                       --   function.   deriving (Show, Eq, Typeable)  instance Exception SeldaError@@ -166,16 +169,24 @@ tableInfo t = TableInfo   { tableColumnInfos = map fromColInfo (tableCols t)   , tableUniqueGroups = uniqueGroups-  , tablePrimaryKey = pkGroups+  , tablePrimaryKey = pkGroup   }   where     uniqueGroups =       [ map (Table.colName . ((tableCols t) !!)) ixs       | (ixs, Unique) <- tableAttrs t       ]-    pkGroups = concat-      [ map (Table.colName . ((tableCols t) !!)) ixs-      | (ixs, Primary) <- tableAttrs t+    pkGroup = nub $ concat+      [ concat+        [ map (Table.colName . ((tableCols t) !!)) ixs+        | (ixs, attr) <- tableAttrs t+        , isPrimary attr+        ]+      , [ Table.colName col+        | col <- tableCols t+        , attr <- Table.colAttrs col+        , isPrimary attr+        ]       ]  -- | A collection of functions making up a Selda backend.@@ -219,11 +230,6 @@   }  -- | Some monad with Selda SQL capabilitites.------   Note that the default implementations of 'invalidateTable' and---   'wrapTransaction' flush the entire cache and disable caching when---   invoked. If you want to use Selda's built-in caching mechanism, you will---   need to implement these operations yourself. class MonadIO m => MonadSelda m where   {-# MINIMAL withConnection #-} 
src/Database/Selda/Exp.hs view
@@ -33,6 +33,7 @@   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+  Raw     :: !Text -> Exp sql a   AggrEx  :: !Text -> !(Exp sql a) -> Exp sql b   InList  :: !(Exp sql a) -> ![Exp sql a] -> Exp sql Bool   InQuery :: !(Exp sql a) -> !sql -> Exp sql Bool@@ -84,6 +85,7 @@   allNamesIn (AggrEx _ x)  = allNamesIn x   allNamesIn (InList x xs) = concatMap allNamesIn (x:xs)   allNamesIn (InQuery x q) = allNamesIn x ++ allNamesIn q+  allNamesIn (Raw _)       = []  instance Names sql => Names (SomeCol sql) where   allNamesIn (Some c)    = allNamesIn c
src/Database/Selda/Frontend.hs view
@@ -247,6 +247,9 @@ --   Use with extreme caution, preferably only for migrations. -- --   On the PostgreSQL backend, at least PostgreSQL 9.6 is required.+--+--   Using this should be avoided in favor of deferred foreign key+--   constraints. See SQL backend documentation for deferred constraints. withoutForeignKeyEnforcement :: (MonadSelda m, MonadMask m) => m a -> m a withoutForeignKeyEnforcement m = withBackend $ \b -> do   bracket_ (liftIO $ disableForeignKeys b True)
src/Database/Selda/SQL.hs view
@@ -1,21 +1,35 @@ {-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, RecordWildCards #-} {-# LANGUAGE TypeOperators, FlexibleInstances, UndecidableInstances #-}-{-# LANGUAGE RankNTypes, CPP #-}+{-# LANGUAGE RankNTypes, CPP, MultiParamTypeClasses #-} -- | SQL AST and parameters for prepared statements. module Database.Selda.SQL where+import Data.String+import Data.Text (Text) import Database.Selda.Exp import Database.Selda.SqlType import Database.Selda.Types #if !MIN_VERSION_base(4, 11, 0)-import Data.Monoid hiding (Product)+import Data.Semigroup (Semigroup (..)) #endif +instance Semigroup QueryFragment where+  (<>) = RawCat++data QueryFragment where+  RawText :: !Text -> QueryFragment+  RawExp  :: !(Exp SQL a) -> QueryFragment+  RawCat  :: !QueryFragment -> !QueryFragment -> QueryFragment++instance IsString QueryFragment where+  fromString = RawText . fromString+ -- | A source for an SQL query. data SqlSource  = TableName !TableName  | Product ![SQL]  | Join !JoinType !(Exp SQL Bool) !SQL !SQL  | Values ![SomeCol SQL] ![[Param]]+ | RawSql !QueryFragment  | EmptyTable  -- | Type of join to perform.@@ -32,11 +46,17 @@   , distinct  :: !Bool   } +instance Names QueryFragment where+  allNamesIn (RawText _)  = []+  allNamesIn (RawExp e)   = allNamesIn e+  allNamesIn (RawCat a b) = allNamesIn a ++ allNamesIn b+ instance Names SqlSource where   allNamesIn (Product qs)   = concatMap allNamesIn qs   allNamesIn (Join _ e l r) = allNamesIn e ++ concatMap allNamesIn [l, r]   allNamesIn (Values vs _)  = allNamesIn vs   allNamesIn (TableName _)  = []+  allNamesIn (RawSql r)     = allNamesIn r   allNamesIn (EmptyTable)   = []  instance Names SQL where
src/Database/Selda/SQL/Print.hs view
@@ -46,6 +46,10 @@ compExp :: PPConfig -> Exp SQL a -> (Text, [Param]) compExp cfg = runPP cfg . ppCol +-- | Compile a raw SQL fragment.+compRaw :: PPConfig -> QueryFragment -> (Text, [Param])+compRaw cfg = runPP cfg . ppRaw+ -- | Compile an @UPATE@ statement. compUpdate :: PPConfig            -> TableName@@ -125,6 +129,10 @@     ppSrc EmptyTable = do       qn <- freshQueryName       pure $ " FROM (SELECT NULL LIMIT 0) AS " <> qn+    ppSrc (RawSql raw) = do+      s <- ppRaw raw+      qn <- freshQueryName+      pure (" FROM (" <> s <> ") AS " <> qn)     ppSrc (TableName n)  = do       pure $ " FROM " <> fromTableName n     ppSrc (Product [])   = do@@ -187,6 +195,11 @@      ppInt = Text.pack . show +ppRaw :: QueryFragment -> PP Text+ppRaw (RawText t)  = pure t+ppRaw (RawExp e)   = ppCol e+ppRaw (RawCat a b) = liftM2 (<>) (ppRaw a) (ppRaw b)+ ppSomeCol :: SomeCol SQL -> PP Text ppSomeCol (Some c)    = ppCol c ppSomeCol (Named n c) = do@@ -214,6 +227,7 @@ ppCol (BinOp op a b) = ppBinOp op a b ppCol (UnOp op a)    = ppUnOp op a ppCol (NulOp a)      = ppNulOp a+ppCol (Raw e)        = pure e ppCol (Fun2 f a b)   = do   a' <- ppCol a   b' <- ppCol b
src/Database/Selda/Transform.hs view
@@ -13,6 +13,7 @@       EmptyTable      -> sql'       TableName _     -> sql'       Values  _ _     -> sql'+      RawSql _        -> sql'       Product qs      -> sql' {source = Product $ map noDead qs}       Join jt on l r  -> sql' {source = Join jt on (noDead l) (noDead r)}   where
src/Database/Selda/Unsafe.hs view
@@ -6,11 +6,20 @@   , aggr   , cast, castAggr, sink, sink2   , unsafeSelector+  , QueryFragment, inj, injLit, rawName, rawExp, rawStm, rawQuery, rawQuery1   ) where+import Control.Exception (throw)+import Control.Monad.State.Strict+import Database.Selda.Backend.Internal import Database.Selda.Column+import Database.Selda.Exp (UntypedCol (..)) import Database.Selda.Inner (Inner, Aggr, aggr, liftAggr) import Database.Selda.Selectors (unsafeSelector)-import Database.Selda.SqlType+import Database.Selda.Query.Type (Query (..), sources, renameAll, rename)+import Database.Selda.SQL (QueryFragment (..), SqlSource (RawSql), sqlFrom)+import Database.Selda.SQL.Print (compRaw)+import Database.Selda.SqlRow (SqlRow (..))+import Database.Selda.Types (ColName) import Data.Text (Text) import Data.Proxy import Unsafe.Coerce@@ -69,3 +78,57 @@ -- | Like 'fun', but with zero arguments. fun0 :: Text -> Col s a fun0 = One . NulOp . Fun0++-- | Create a raw SQL query fragment from the given column.+inj :: Col s a -> QueryFragment+inj (One x) = RawExp x++-- | Create a raw SQL query fragment from the given value.+injLit :: SqlType a => a -> QueryFragment+injLit = RawExp . Lit . mkLit++-- | Create a column referring to a name of your choice.+--   Use this to refer to variables not exposed by Selda.+rawName :: SqlType a => ColName -> Col s a+rawName = One . Col++-- | Create an expression from the given text.+--   The expression will be inserted verbatim into your query, so you should+--   NEVER pass user-provided text to this function.+rawExp :: SqlType a => Text -> Col s a+rawExp = One . Raw++-- | Execute a raw SQL statement.+rawStm :: MonadSelda m => QueryFragment -> m ()+rawStm q = withBackend $ \b -> liftIO $ do+  void $ uncurry (runStmt b) $ compRaw (ppConfig b) q++-- | Execute a raw SQL statement, returning a row consisting of columns by the+--   given names.+--   Will fail if the number of names given does not match up with+--   the type of the returned row.+--   Will generate invalid SQL if the given names don't match up with the+--   column names in the given query.+rawQuery :: forall a s. SqlRow a => [ColName] -> QueryFragment -> Query s (Row s a)+rawQuery names q+  | length names /= nestedCols (Proxy :: Proxy a) = do+      let err = concat+            [ "rawQuery: return type has ", show (nestedCols (Proxy :: Proxy a))+            , " columns, but only ", show (length names), " names were given"+            ]+      throw (UnsafeError err)+  | otherwise = Query $ do+      rns <- renameAll [Untyped (Col name) | name <- names]+      st <- get+      put $ st { sources = sqlFrom rns (RawSql q) : sources st }+      return (Many (map hideRenaming rns))++-- | As 'rawQuery', but returns only a single column. Same warnings still apply.+rawQuery1 :: SqlType a => ColName -> QueryFragment -> Query s (Col s a)+rawQuery1 name q = Query $ do+  name' <- head <$> rename (Untyped (Col name))+  st <- get+  put $ st { sources = sqlFrom [name'] (RawSql q) : sources st }+  case name' of+    Named n _ -> return (One (Col n))+    _         -> error "BUG: renaming did not rename"