selda 0.1.0.0 → 0.1.1.0
raw patch · 17 files changed
+579/−119 lines, 17 filesdep ~base
Dependency ranges changed: base
Files
- README.md +16/−15
- selda.cabal +2/−1
- src/Database/Selda.hs +28/−15
- src/Database/Selda/Backend.hs +5/−1
- src/Database/Selda/Column.hs +3/−4
- src/Database/Selda/Compile.hs +8/−15
- src/Database/Selda/Frontend.hs +24/−21
- src/Database/Selda/Generic.hs +349/−0
- src/Database/Selda/Query.hs +28/−4
- src/Database/Selda/Query/Type.hs +16/−3
- src/Database/Selda/SQL.hs +31/−2
- src/Database/Selda/SQL/Print.hs +18/−0
- src/Database/Selda/SqlType.hs +10/−3
- src/Database/Selda/Table.hs +1/−19
- src/Database/Selda/Table/Compile.hs +37/−16
- src/Database/Selda/Transform.hs +2/−0
- src/Database/Selda/Types.hs +1/−0
README.md view
@@ -10,12 +10,14 @@ Features ======== +* Monadic interface: no need to be a category theory wizard just to write a few+ database queries.+* Portable: fully functional backends for SQLite and PostgreSQL. * Creating, dropping and querying tables using type-safe database schemas.-* Monadic query language with products, filtering, joins and aggregation.+* Typed query language with products, filtering, joins and aggregation. * Inserting, updating and deleting rows from tables. * Transaction support. * Configurable, automatic, consistent in-process caching of query results.-* Multiple backends: SQLite and PostgreSQL. * Lightweight and modular: non-essential features are optional or split into add-on packages. @@ -158,15 +160,11 @@ number of inserted rows, and one appended with an underscore which returns `()`. Use the latter to explicitly indicate your intent to ignore the return value. -There is one gotcha when inserting tuples: auto-incrementing primary keys.-When inserting data into a table with such a primary key, *the primary key-column must be omitted from any inserted tuples* to avoid a type error.-The reason for this is that it's usually a bad idea to set auto-incrementing-keys manually. The following example inserts a few rows into a table with an+The following example inserts a few rows into a table with an auto-incrementing primary key: ```-people' :: Table (Auto Int :*: Text :*: Int :*: Maybe Text)+people' :: Table (Int :*: Text :*: Int :*: Maybe Text) people' = table "people_with_ids" $ autoPrimary "id" ¤ required "name"@@ -176,15 +174,15 @@ populate' :: SeldaT IO () populate' = do insert_ people'- [ "Link" :*: 125 :*: Just "horse"- , "Velvet" :*: 19 :*: Nothing- , "Kobayashi" :*: 23 :*: Just "dragon"- , "Miyu" :*: 10 :*: Nothing+ [ def :*: "Link" :*: 125 :*: Just "horse"+ , def :*: "Velvet" :*: 19 :*: Nothing+ , def :*: "Kobayashi" :*: 23 :*: Just "dragon"+ , def :*: "Miyu" :*: 10 :*: Nothing ] ``` -Note that the tuple list passed to `insert_` is the same as for the previous-example, even though our new table has an additional field `id`.+Note the use of the `def` value for the `id` field. This indicates that the+default value for the column should be used in lieu of any user-provided value. Since the `id` field is an auto-incrementing primary key, it will automatically be assigned a unique, increasing value. Thus, the resulting table would look like this:@@ -198,7 +196,9 @@ 3 | Miyu | 10 | ``` -If you want manual control over your primary keys, do not use `autoPrimary`.+Also note that `def` can *only* be used for columns that have default values.+Currently, only auto-incrementing primary keys can have defaults.+Attempting to use `def` in any other context results in a runtime error. Updating rows@@ -498,3 +498,4 @@ * Constraints other than primary key. * Database schema upgrades. * Stack build.+* MySQL/MariaDB backend.
selda.cabal view
@@ -1,5 +1,5 @@ name: selda-version: 0.1.0.0+version: 0.1.1.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,@@ -46,6 +46,7 @@ exposed-modules: Database.Selda Database.Selda.Backend+ Database.Selda.Generic Database.Selda.Unsafe other-modules: Database.Selda.Caching
src/Database/Selda.hs view
@@ -13,7 +13,8 @@ , Text, Cols, Columns , Order (..) , (:*:)(..)- , select, restrict, limit, order+ , select, selectValues+ , restrict, limit, order , ascending, descending -- * Expressions over columns , (.==), (./=), (.>), (.<), (.>=), (.<=), like@@ -28,14 +29,13 @@ , aggregate, groupBy , count, avg, sum_, max_, min_ -- * Modifying tables- , Insert, InsertCols, HasAutoPrimary- , insert, insert_, insertWithPK+ , Insert+ , insert, insert_, insertWithPK, def , update, update_ , deleteFrom, deleteFrom_ -- * Defining schemas , ColSpec, TableName, ColName , NonNull, IsNullable, Nullable, NotNullable- , Auto , table, (¤), required, optional , primary, autoPrimary -- * Combining schemas@@ -60,20 +60,21 @@ import Database.Selda.Inner import Database.Selda.Query import Database.Selda.Query.Type-import Database.Selda.SQL (Order (..))+import Database.Selda.SQL import Database.Selda.SqlType import Database.Selda.Table import Database.Selda.Table.Compile import Database.Selda.Types import Database.Selda.Unsafe+import Control.Exception (throw) -- | Any column type that can be used with the 'min_' and 'max_' functions.-class MinMax a-instance {-# OVERLAPPABLE #-} Num a => MinMax a+class SqlType a => MinMax a+instance {-# OVERLAPPABLE #-} (SqlType a, Num a) => MinMax a instance MinMax Text instance MinMax a => MinMax (Maybe a) -(.==), (./=), (.>), (.<), (.>=), (.<=) :: Col s a -> Col s a -> Col s Bool+(.==), (./=), (.>), (.<), (.>=), (.<=) :: SqlType a => Col s a -> Col s a -> Col s Bool (.==) = liftC2 $ BinOp Eq (./=) = liftC2 $ BinOp Neq (.>) = liftC2 $ BinOp Gt@@ -101,6 +102,15 @@ ascending = Asc descending = Desc +-- | The default value for a column during insertion.+-- For an auto-incrementing primary key, the default value is the next key.+--+-- Using @def@ in any other context than insertion results in a runtime error.+-- Likewise, if @def@ is given for a column that does not have a default+-- value, the insertion will fail.+def :: SqlType a => a+def = throw DefaultValueException+ -- | Lift a non-nullable column to a nullable one. -- Useful for creating expressions over optional columns: --@@ -111,7 +121,7 @@ -- > name :*: _ :*: pet <- select people -- > restrict (pet .== just "cat") -- > return name-just :: Col s a -> Col s (Maybe a)+just :: SqlType a => Col s a -> Col s (Maybe a) just = cast -- | SQL NULL, at any type you like.@@ -135,17 +145,20 @@ true = literal True false = literal False --- | The SQL @LIKE@ operator; matches strings with wildcards.+-- | The SQL @LIKE@ operator; matches strings with @%@ wildcards.+-- For instance:+--+-- > "%gon" `like` "dragon" .== true like :: Col s Text -> Col s Text -> Col s Bool like = liftC2 $ BinOp Like infixl 4 `like` -- | The number of non-null values in the given column.-count :: Col s a -> Aggr s Int+count :: SqlType a => Col s a -> Aggr s Int count = aggr "COUNT" -- | The average of all values in the given column.-avg :: Num a => Col s a -> Aggr s a+avg :: (SqlType a, Num a) => Col s a -> Aggr s a avg = aggr "AVG" -- | The greatest value in the given column. Texts are compared lexically.@@ -157,7 +170,7 @@ min_ = aggr "MIN" -- | Sum all values in the given column.-sum_ :: Num a => Col s a -> Aggr s a+sum_ :: (SqlType a, Num a) => Col s a -> Aggr s a sum_ = aggr "SUM" -- | Round a value to the nearest integer. Equivalent to @roundTo 0@.@@ -177,11 +190,11 @@ not_ = liftC $ UnOp Not -- | Convert a boolean column to any numeric type.-fromBool :: Num a => Col s Bool -> Col s a+fromBool :: (SqlType a, Num a) => Col s Bool -> Col s a fromBool = cast -- | Convert an integer column to any numeric type.-fromInt :: Num a => Col s Int -> Col s a+fromInt :: (SqlType a, Num a) => Col s Int -> Col s a fromInt = cast -- | Convert any column to a string.
src/Database/Selda/Backend.hs view
@@ -34,7 +34,11 @@ -- | Generate a custom column type for the column having the given Selda -- type and list of attributes. , customColType :: Text -> [ColAttr] -> Maybe Text- }++ -- | The keyword that represents the default value for auto-incrementing+ -- primary keys.+ , defaultKeyword :: Text+} -- | Some monad with Selda SQL capabilitites. class MonadIO m => MonadSelda m where
src/Database/Selda/Column.hs view
@@ -2,16 +2,13 @@ -- | Columns and associated utility functions. module Database.Selda.Column where import Database.Selda.SqlType-import Database.Selda.Table (Auto) import Database.Selda.Types import Data.String import Data.Text (Text) -- | Convert a tuple of Haskell types to a tuple of column types. type family Cols s a where- Cols s (Auto a :*: b) = Col s a :*: Cols s b Cols s (a :*: b) = Col s a :*: Cols s b- Cols s (Auto a) = Col s a Cols s a = Col s a -- | Any column tuple.@@ -27,7 +24,7 @@ instance Columns (Col s a) where toTup [x] = C (Col x) toTup [] = error "too few elements to toTup"- toTup _ = error "too many elements to toTup"+ toTup xs = C (TblCol xs) fromTup (C x) = [Some x] -- | A type-erased column, which may also be renamed.@@ -58,6 +55,7 @@ -- | Underlying column expression type, not tied to any particular query. data Exp a where Col :: !ColName -> Exp a+ TblCol :: ![ColName] -> Exp a Lit :: !(Lit a) -> Exp a BinOp :: !(BinOp a b) -> !(Exp a) -> !(Exp a) -> Exp b UnOp :: !(UnOp a b) -> !(Exp a) -> Exp b@@ -67,6 +65,7 @@ -- | Get all column names in the given expression. allNamesIn :: Exp a -> [ColName]+allNamesIn (TblCol ns) = ns allNamesIn (Col n) = [n] allNamesIn (Lit _) = [] allNamesIn (BinOp _ a b) = allNamesIn a ++ allNamesIn b
src/Database/Selda/Compile.hs view
@@ -11,6 +11,7 @@ import Database.Selda.Table.Compile import Database.Selda.Transform import Database.Selda.Types+import Data.Maybe (catMaybes) import Data.Proxy import Data.Text (Text, empty) import Data.Typeable@@ -24,13 +25,14 @@ compileWithTables :: Result a => Query s a -> ([TableName], (Text, [Param])) compileWithTables = compSql . snd . compQuery --- | Compile an @INSERT@ query.-compileInsert :: Insert (InsertCols a)- => Table a -> [InsertCols a] -> (Text, [Param])-compileInsert _ [] = (empty, [])-compileInsert tbl rows = (compInsert tbl nrows, concat ps)+-- | Compile an @INSERT@ query, given the keyword representing default values+-- in the target SQL dialect, a table and a list of items corresponding+-- to the table.+compileInsert :: Insert a => Text -> Table a -> [a] -> (Text, [Param])+compileInsert _ _ [] = (empty, [])+compileInsert defkw tbl rows = (compInsert defkw tbl defs, catMaybes $ concat ps) where ps = map params rows- nrows = length rows+ defs = map (map (maybe True (const False))) ps -- | Compile an @UPDATE@ query. compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a))@@ -63,15 +65,6 @@ sql = state2sql st live = colNames final ++ allNonOutputColNames sql srcs = removeDeadCols live sql---- | An extensible tuple of Haskell-level values (i.e. @Int :*: Maybe Text@)--- which can be inserted into a table.-class Insert a where- params :: a -> [Param]-instance (SqlType a, Insert b) => Insert (a :*: b) where- params (a :*: b) = Param (mkLit a) : params b-instance {-# OVERLAPPABLE #-} SqlType a => Insert a where- params a = [Param (mkLit a) ] -- | An acceptable query result type; one or more columns stitched together -- with @:*:@.
src/Database/Selda/Frontend.hs view
@@ -15,6 +15,7 @@ import Database.Selda.Column import Database.Selda.Compile import Database.Selda.Query.Type+import Database.Selda.SQL import Database.Selda.Table import Database.Selda.Table.Compile import Data.Proxy@@ -22,7 +23,8 @@ import Control.Monad import Control.Monad.Catch --- | Run a query within a Selda transformer.+-- | Run a query within a Selda monad. In practice, this is often a 'SeldaT'+-- transformer on top of some other monad. -- Selda transformers are entered using backend-specific @withX@ functions, -- such as 'withSQLite' from the SQLite backend. query :: (MonadSelda m, Result a) => Query s a -> m [Res a]@@ -31,8 +33,9 @@ queryWith (runStmt backend) q -- | Insert the given values into the given table. All columns of the table--- must be present, EXCEPT any auto-incrementing primary keys ('autoPrimary'--- columns), which are always assigned their default value.+-- must be present. If your table has an auto-incrementing primary key,+-- use the special value 'def' for that column to get the auto-incrementing+-- behavior. -- Returns the number of rows that were inserted. -- -- To insert a list of tuples into a table with auto-incrementing primary key:@@ -46,37 +49,34 @@ -- > -- > main = withSQLite "my_database.sqlite" $ do -- > insert_ people--- > [ "Link" :*: 125 :*: Just "horse"--- > , "Zelda" :*: 119 :*: Nothing+-- > [ def :*: "Link" :*: 125 :*: Just "horse"+-- > , def :*: "Zelda" :*: 119 :*: Nothing -- > , ... -- > ]------ Again, note that ALL non-auto-incrementing fields must be present in the--- tuples to be inserted, including primary keys without the auto-increment--- attribute.-insert :: (MonadSelda m, Insert (InsertCols a))- => Table a -> [InsertCols a] -> m Int+insert :: (MonadSelda m, Insert a) => Table a -> [a] -> m Int insert _ [] = do return 0 insert t cs = do+ kw <- defaultKeyword <$> seldaBackend+ res <- uncurry exec $ compileInsert kw t cs liftIO $ invalidate (tableName t)- uncurry exec $ compileInsert t cs+ return res -- | Like 'insert', but does not return anything. -- Use this when you really don't care about how many rows were inserted.-insert_ :: (MonadSelda m, Insert (InsertCols a))- => Table a -> [InsertCols a] -> m ()+insert_ :: (MonadSelda m, Insert a) => Table a -> [a] -> m () insert_ t cs = void $ insert t cs -- | Like 'insert', but returns the primary key of the last inserted row. -- Attempting to run this operation on a table without an auto-incrementing -- primary key is a type error.-insertWithPK :: (MonadSelda m, HasAutoPrimary a, Insert (InsertCols a))- => Table a -> [InsertCols a] -> m Int+insertWithPK :: (MonadSelda m, Insert a) => Table a -> [a] -> m Int insertWithPK t cs = do backend <- seldaBackend- liftIO $ invalidate (tableName t)- liftIO . uncurry (runStmtWithPK backend) $ compileInsert t cs+ liftIO $ do+ res <- uncurry (runStmtWithPK backend) $ compileInsert (defaultKeyword backend) t cs+ invalidate (tableName t)+ return res -- | Update the given table using the given update function, for all rows -- matching the given predicate. Returns the number of updated rows.@@ -86,8 +86,9 @@ -> (Cols s a -> Cols s a) -- ^ Update function. -> m Int update tbl check upd = do+ res <- uncurry exec $ compileUpdate tbl upd check liftIO $ invalidate (tableName tbl)- uncurry exec $ compileUpdate tbl upd check+ return res -- | Like 'update', but doesn't return the number of updated rows. update_ :: (MonadSelda m, Columns (Cols s a), Result (Cols s a))@@ -102,8 +103,9 @@ deleteFrom :: (MonadSelda m, Columns (Cols s a)) => Table a -> (Cols s a -> Col s Bool) -> m Int deleteFrom tbl f = do+ res <- uncurry exec $ compileDelete tbl f liftIO $ invalidate (tableName tbl)- uncurry exec $ compileDelete tbl f+ return res -- | Like 'deleteFrom', but does not return the number of deleted rows. deleteFrom_ :: (MonadSelda m, Columns (Cols s a))@@ -182,8 +184,9 @@ -- results depending on that table. withInval :: MonadSelda m => (Table a -> m b) -> Table a -> m b withInval f t = do+ res <- f t liftIO $ invalidate $ tableName t- f t+ return res -- | Execute a statement without a result. exec :: MonadSelda m => Text -> [Param] -> m Int
+ src/Database/Selda/Generic.hs view
@@ -0,0 +1,349 @@+{-# LANGUAGE TypeFamilies, TypeOperators, FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ConstraintKinds #-}+-- | Build tables and database operations from (almost) any Haskell type.+--+-- While the types in this module may look somewhat intimidating, the rules+-- for generic tables and queries are quite simple:+--+-- * Any record type with a single data constructor, where all fields are+-- instances of 'SqlType', can be used for generic tables and queries+-- if it derives 'Generic'.+-- * To use the standard functions from "Database.Selda" on a generic table,+-- it needs to be unwrapped using 'gen'.+-- * Performing a 'select' on a generic table returns all the table's fields+-- as an inductive tuple.+-- * Tuples obtained this way can be handled either as any other tuple, or+-- using the '(!)' operator together with any record selector for the+-- tuple's corresponding type.+-- * Relations obtained from a query can be re-assembled into their+-- corresponding data type using 'fromRel'.+module Database.Selda.Generic+ ( Relational, Generic+ , GenTable (..), Attribute, Relation+ , genTable, toRel, fromRel, (!)+ , insertGen, insertGen_, insertGenWithPK+ , primaryGen, autoPrimaryGen+ ) where+import Control.Monad.State+import Data.Dynamic+import Data.Text (pack)+import GHC.Generics hiding (R, (:*:))+import qualified GHC.Generics as G ((:*:)(..))+import Unsafe.Coerce+import Database.Selda+import Database.Selda.Column+import Database.Selda.Table+import Database.Selda.SqlType++-- | Any type which has a corresponding relation.+-- To make a @Relational@ instance for some type, simply derive 'Generic'.+--+-- Note that only types which have a single data constructor, and where all+-- fields are instances of 'SqlValue' can be used with this module.+-- Attempting to use functions in this module with any type which doesn't+-- obey those constraints will result in a very confusing type error.+type Relational a =+ ( Generic a+ , GRelation (Rep a)+ , GFromRel (Rep a)+ , ToDyn (Relation a)+ , Insert (Relation a)+ )++-- | A generic table. Needs to be unpacked using @gen@ before use with+-- 'select', 'insert', etc.+newtype GenTable a = GenTable {gen :: Table (Relation a)}++-- | The relation corresponding to the given Haskell type.+-- This relation simply corresponds to the fields in the data type, from+-- left to right. For instance:+--+-- > data Foo = Foo+-- > { bar :: Int+-- > , baz :: Text+-- > }+--+-- In this example, @Relation Foo@ is @(Int :*: Text)@, as the first field+-- of @Foo@ has type @Int@, and the second has type @Text@.+type Relation a = Rel (Rep a)++-- | Generate a table from the given table name and list of column attributes.+-- All @Maybe@ fields in the table's type will be represented by nullable+-- columns, and all non-@Maybe@ fields fill be represented by required+-- columns.+-- For example:+--+-- > data Person = Person+-- > { id :: Int+-- > , name :: Text+-- > , age :: Int+-- > , pet :: Maybe Text+-- > }+-- > deriving Generic+-- >+-- > people :: GenTable Person+-- > people = genTable "people" [(name, autoPrimaryGen)]+--+-- This example will create a table with the column types+-- @Int :*: Text :*: Int :*: Maybe Text@, where the first field is+-- an auto-incrementing primary key.+genTable :: forall a b. Relational a+ => TableName+ -> [(a -> b, Attribute)]+ -> GenTable a+genTable tn attrs = GenTable $ Table tn (validate tn (map tidy cols))+ where+ dummy = mkDummy+ cols = zipWith addAttrs [0..] (tblCols (Proxy :: Proxy a))+ addAttrs n ci = ci+ { colAttrs = colAttrs ci ++ concat+ [ as+ | (f, Attribute as) <- attrs+ , identify dummy f == n+ ]+ }++-- | Convert a generic type into the corresponding database relation.+-- A type's corresponding relation is simply the inductive tuple consisting+-- of all of the type's fields.+--+-- > data Person = Person+-- > { id :: Auto Int+-- > , name :: Text+-- > , age :: Int+-- > , pet :: Maybe Text+-- > }+-- > deriving Generic+-- >+-- > somePerson = Person 0 "Velvet" 19 Nothing+-- > (theId :*: theName :*: theAge :*: thePet) = toRel somePerson+--+-- This is mainly useful when inserting values into a table using 'insert'+-- and the other functions from "Database.Selda".+-- Note that since @toRel@ doesn't filter out auto-incrementing primary key+-- fields, you should use 'insertGen' and friends to insert values into+-- tables with auto-incrementing primary keys instead.+toRel :: Relational a => a -> Relation a+toRel = gToRel . from++-- | Re-assemble a generic type from its corresponding relation. This can be+-- done either for ad hoc queries or for queries over generic tables:+--+-- > data SimplePerson = SimplePerson+-- > { name :: Text+-- > , age :: Int+-- > }+-- > deriving Generic+-- >+-- > demoPerson :: SimplePerson+-- > demoPerson = fromRel ("Miyu" :*: 10)+-- >+-- > adhoc :: Table (Text :*: Int)+-- > adhoc = table "adhoc" $ required "name" ¤ required "age"+-- >+-- > getPersons1 :: MonadSelda m => m [SimplePerson]+-- > getPersons1 = map fromRel <$> query (select adhoc)+-- >+-- > generic :: GenTable SimplePerson+-- > generic = genTable "generic" []+-- >+-- > getPersons2 :: MonadSelda m => m [SimplePerson]+-- > getPersons2 = map fromRel <$> query (select (gen generic))+--+-- Applying @toRel@ to an inductive tuple which isn't the corresponding+-- relation of the return type is a type error.+fromRel :: Relational a => Relation a -> a+fromRel = to . fst . gFromRel . toD++-- | Like 'insertWithPK', but accepts a generic table and+-- its corresponding data type.+insertGenWithPK :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m Int+insertGenWithPK t = insertWithPK (gen t) . map toRel++-- | Like 'insert', but accepts a generic table and its corresponding data type.+insertGen :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m Int+insertGen t = insert (gen t) . map toRel++-- | Like 'insert_', but accepts a generic table and its corresponding data type.+insertGen_ :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m ()+insertGen_ t = void . insertGen t++-- | From the given table column, get the column corresponding to the given+-- selector function. For instance:+--+-- > data Person = Person+-- > { id :: Auto Int+-- > , name :: Text+-- > , age :: Int+-- > , pet :: Maybe Text+-- > }+-- > deriving Generic+-- >+-- > people :: Table Person+-- > people = genTable "people" [name :- primary]+-- >+-- > getAllAges :: Query s Int+-- > getAllAges = do+-- > p <- select people+-- > return (p ! age)+--+-- Note that ONLY selector functions may be passed as the second argument of+-- this function. Attempting to pass any non-selector function results in a+-- Haskell runtime error.+(!) :: (Columns (Cols s (Relation a)), Relational a, SqlType b)+ => Cols s (Relation a) -> (a -> b) -> Col s b+cs ! f =+ case drop (identify mkDummy f) cols of+ (Named x _ : _) -> C (Col x)+ (Some c : _) -> C (unsafeCoerce c)+ _ -> error "attempted to use a non-selector with (!)"+ where+ cols = fromTup cs++-- | Some attribute that may be set on a table column.+newtype Attribute = Attribute [ColAttr]++-- | A primary key which does not auto-increment.+primaryGen :: Attribute+primaryGen = Attribute [Primary, Required]++-- | An auto-incrementing primary key.+autoPrimaryGen :: Attribute+autoPrimaryGen = Attribute [Primary, AutoIncrement, Required]++-- | A dummy of some type. Encapsulated to avoid improper use, since all of+-- its fields are 'unsafeCoerce'd ints.+newtype Dummy a = Dummy a++-- | Extract all column names from the given type.+-- If the type is not a record, the columns will be named @col_1@,+-- @col_2@, etc.+tblCols :: forall a. (GRelation (Rep a)) => Proxy a -> [ColInfo]+tblCols _ = zipWith pack' [0 :: Int ..] $ gTblCols (Proxy :: Proxy (Rep a))+ where+ pack' n ci = ci+ { colName = if colName ci == ""+ then pack $ "col_" ++ show n+ else colName ci+ }++-- | Create a dummy of the given type.+mkDummy :: (Generic a, GRelation (Rep a)) => Dummy a+mkDummy = Dummy $ to $ evalState gMkDummy 0++-- | Get the selector identifier of the given selector for the given dummy.+identify :: Dummy a -> (a -> b) -> Int+identify (Dummy d) f = unsafeCoerce $ f d++class Traits a where+ isMaybeType :: Proxy a -> Bool+ isMaybeType _ = False+instance Traits (Maybe a) where+ isMaybeType _ = True+instance {-# OVERLAPPABLE #-} Traits a++-- | Normalized append of two inductive tuples.+-- Note that this will flatten any nested inductive tuples.+type family a :++: b where+ (a :*: b) :++: c = a :*: (b :++: c)+ a :++: b = a :*: b++class Append a b where+ app :: a -> b -> a :++: b++instance {-# OVERLAPPING #-} Append b c => Append (a :*: b) c where+ app (a :*: b) c = a :*: app b c++instance ((a :*: b) ~ (a :++: b)) => Append a b where+ app a b = a :*: b++-- | The relation corresponding to the given type.+type family Rel (rep :: * -> *) where+ Rel (M1 t c a) = Rel a+ Rel (K1 i a) = a+ Rel (a G.:*: b) = Rel a :++: Rel b++class GRelation f where+ -- | Convert a value from its Haskell type into the corresponding relation.+ gToRel :: f a -> Rel f++ -- | Compute all columns needed to represent the given type.+ gTblCols :: Proxy f -> [ColInfo]++ -- | Create a dummy value where all fields are replaced by @unsafeCoerce@'d+ -- ints. See 'mkDummy' and 'identify' for more information.+ gMkDummy :: State Int (f a)++instance GRelation a => GRelation (M1 C c a) where+ gToRel (M1 x) = gToRel x+ gTblCols _ = gTblCols (Proxy :: Proxy a)+ gMkDummy = M1 <$> gMkDummy++instance GRelation a => GRelation (M1 D c a) where+ gToRel (M1 x) = gToRel x+ gTblCols _ = gTblCols (Proxy :: Proxy a)+ gMkDummy = M1 <$> gMkDummy++instance (Selector c, GRelation a) => GRelation (M1 S c a) where+ gToRel (M1 x) = gToRel x+ gTblCols _ = [ci']+ where+ [ci] = gTblCols (Proxy :: Proxy a)+ ci' = ColInfo+ { colName = pack $ selName ((M1 undefined) :: M1 S c a b)+ , colType = colType ci+ , colAttrs = colAttrs ci+ }+ gMkDummy = M1 <$> gMkDummy++instance (Traits a, SqlType a) => GRelation (K1 i a) where+ gToRel (K1 x) = x+ gTblCols _ = [ColInfo "" (sqlType (Proxy :: Proxy a)) optReq]+ where+ optReq+ | isMaybeType (Proxy :: Proxy a) = [Optional]+ | otherwise = [Required]+ gMkDummy = do+ n <- get+ put (n+1)+ return $ unsafeCoerce n++instance (Append (Rel a) (Rel b), GRelation a, GRelation b) =>+ GRelation (a G.:*: b) where+ gToRel (a G.:*: b) = gToRel a `app` gToRel b+ gTblCols _ = gTblCols a ++ gTblCols b+ where+ a = Proxy :: Proxy a+ b = Proxy :: Proxy b+ gMkDummy = do+ a <- gMkDummy :: State Int (a x)+ b <- gMkDummy :: State Int (b x)+ return (a G.:*: b)+++class Typeable a => ToDyn a where+ toD :: a -> [Dynamic]+instance (Typeable a, ToDyn b) => ToDyn (a :*: b) where+ toD (a :*: b) = toDyn a : toD b+instance {-# OVERLAPPABLE #-} Typeable a => ToDyn a where+ toD a = [toDyn a]++class GFromRel f where+ -- | Convert a value to a Haskell type from the type's corresponding relation.+ gFromRel :: [Dynamic] -> (f a, [Dynamic])++instance (GFromRel a, GFromRel b) => GFromRel (a G.:*: b) where+ gFromRel xs =+ (x G.:*: y, xs'')+ where+ (x, xs') = gFromRel xs+ (y, xs'') = gFromRel xs'++instance Typeable a => GFromRel (K1 i a) where+ gFromRel (x:xs) = (K1 (fromDyn x (error "impossible")), xs)+ gFromRel _ = error "impossible: too few elements to gFromRel"++instance GFromRel a => GFromRel (M1 t c a) where+ gFromRel xs = (M1 x, xs')+ where (x, xs') = gFromRel xs
src/Database/Selda/Query.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleContexts, OverloadedStrings #-} -- | Query monad and primitive operations. module Database.Selda.Query where import Database.Selda.Column@@ -20,6 +20,30 @@ where cs' = map colName cs +-- | Query an ad hoc table of type @a@. Each element in the given list represents+-- one row in the ad hoc table.+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}+ return $ toTup (repeat "NULL")+selectValues (row:rows) = Query $ do+ names <- mapM (const freshName) rowlist+ let rns = [Named n (Col n) | n <- names]+ r = mkFirstRow names+ st <- get+ put $ st {sources = SQL rns (Values r rs) [] [] [] Nothing : sources st}+ return $ toTup [n | Named n _ <- rns]+ where+ rowlist = map noDef $ params row+ mkFirstRow ns =+ [ Named n (Lit l)+ | (Param l, n) <- zip rowlist ns+ ]+ rs = map (map noDef . params) rows+ noDef Nothing = error "default value given to selectValues"+ noDef (Just x) = x+ -- | Restrict the query somehow. Roughly equivalent to @WHERE@. restrict :: Col s Bool -> Query s () restrict (C p) = Query $ do@@ -49,11 +73,11 @@ -- they return any non-aggregate columns. Attempting to do either results in -- a type error. ----- The SQL @HAVING@ keyword can be implemented by combining @aggregte@+-- The SQL @HAVING@ keyword can be implemented by combining @aggregate@ -- and 'restrict': -- -- > -- Find the number of people living on every address, for all addresses--- < -- with more than one tenant:+-- > -- with more than one tenant: -- > -- SELECT COUNT(name) AS c, address FROM housing GROUP BY name HAVING c > 1 -- > -- > numPpl = do@@ -97,8 +121,8 @@ -- > (select addresses) -- > return (name :*: address) leftJoin :: (Columns a, Columns (OuterCols a), Columns (JoinCols a))- -- | Predicate determining which lines to join. => (OuterCols a -> Col s Bool)+ -- ^ Predicate determining which lines to join. -- | Right-hand query to join. -> Query (Inner s) a -> Query s (JoinCols a)
src/Database/Selda/Query/Type.hs view
@@ -5,6 +5,7 @@ import Data.Text (pack) import Database.Selda.SQL import Database.Selda.Column+import Database.Selda.Types (ColName) -- | An SQL query. newtype Query s a = Query {unQ :: State GenState a}@@ -47,9 +48,8 @@ -- | Generate a unique name for the given column. rename :: SomeCol -> State GenState SomeCol rename (Some col) = do- st <- get- put $ st {nameSupply = succ $ nameSupply st}- return $ Named (newName $ nameSupply st) col+ n <- freshId+ return $ Named (newName n) col where newName ns = case col of@@ -57,3 +57,16 @@ _ -> "tmp_" <> pack (show ns) rename col@(Named _ _) = do return col++-- | Get a guaranteed unique identifier.+freshId :: State GenState Int+freshId = do+ st <- get+ put $ st {nameSupply = succ $ nameSupply st}+ return (nameSupply st)++-- | Get a guaranteed unique column name.+freshName :: State GenState ColName+freshName = do+ n <- freshId+ return $ "tmp_" <> pack (show n)
src/Database/Selda/SQL.hs view
@@ -1,16 +1,21 @@ {-# LANGUAGE GADTs, OverloadedStrings #-}--- | SQL AST and pretty-printing.+{-# LANGUAGE TypeOperators, FlexibleInstances, UndecidableInstances #-}+-- | SQL AST and parameters for prepared statements. module Database.Selda.SQL where import Database.Selda.Column import Database.Selda.SqlType-import Database.Selda.Types (TableName)+import Database.Selda.Types+import Control.Exception import Data.Monoid+import System.IO.Unsafe -- | A source for an SQL query. data SqlSource = TableName !TableName | Product ![SQL] | LeftJoin !(Exp Bool) !SQL !SQL+ | Values ![SomeCol] ![[Param]]+ | EmptyTable -- | AST for SQL queries. data SQL = SQL@@ -37,3 +42,27 @@ Param a == Param b = compLit a b == EQ instance Ord Param where compare (Param a) (Param b) = compLit a b++-- | Exception indicating the use of a default value.+-- If any values throwing this during evaluation of @param xs@ will be+-- replaced by their default value.+data DefaultValueException = DefaultValueException+ deriving Show+instance Exception DefaultValueException++-- | An inductive tuple of Haskell-level values (i.e. @Int :*: Maybe Text@)+-- which can be inserted into a table.+class Insert a where+ params :: a -> [Maybe Param]+instance (SqlType a, Insert b) => Insert (a :*: b) where+ params (a :*: b) = unsafePerformIO $ do+ res <- try $ return $! a+ case res of+ Right a' -> return $ Just (Param (mkLit a')) : params b+ Left DefaultValueException -> return $ Nothing : params b+instance {-# OVERLAPPABLE #-} SqlType a => Insert a where+ params a = unsafePerformIO $ do+ res <- try $ return $! a+ case res of+ Right a' -> return [Just $ Param (mkLit a')]+ Left DefaultValueException -> return [Nothing]
src/Database/Selda/SQL/Print.hs view
@@ -109,6 +109,9 @@ result [] = "1" result cs' = Text.intercalate "," cs' + ppSrc EmptyTable = do+ qn <- freshQueryName+ pure $ " FROM (SELECT NULL LIMIT 0) AS " <> qn ppSrc (TableName n) = do dependOn n pure $ " FROM " <> n@@ -120,6 +123,16 @@ qn <- freshQueryName pure (q <> " AS " <> qn) pure $ " FROM " <> Text.intercalate "," qs+ ppSrc (Values row rows) = do+ row' <- Text.intercalate ", " <$> mapM ppSomeCol row+ rows' <- mapM ppRow rows+ qn <- freshQueryName+ pure $ mconcat+ [ " FROM (SELECT "+ , Text.intercalate " UNION ALL SELECT " (row':rows')+ , ") AS "+ , qn+ ] ppSrc (LeftJoin on left right) = do l' <- ppSql left r' <- ppSql right@@ -132,6 +145,10 @@ , " ON ", on' ] + ppRow xs = do+ ls <- sequence [ppLit l | Param l <- xs]+ pure $ Text.intercalate ", " ls+ ppRestricts [] = pure "" ppRestricts rs = ppCols rs >>= \rs' -> pure $ " WHERE " <> rs' @@ -167,6 +184,7 @@ pure $ "(" <> Text.intercalate ") AND (" cs' <> ")" ppCol :: Exp a -> PP Text+ppCol (TblCol xs) = error $ "compiler bug: ppCol saw TblCol: " ++ show xs ppCol (Col name) = pure name ppCol (Lit l) = ppLit l ppCol (BinOp op a b) = ppBinOp op a b
src/Database/Selda/SqlType.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables #-}+{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, FlexibleInstances #-} -- | Types representable in Selda's subset of SQL. module Database.Selda.SqlType where import Data.Text (Text, pack, unpack)@@ -22,11 +22,11 @@ -- | Any datatype representable in (Selda's subset of) SQL. class SqlType a where- mkLit :: a -> Lit a+ mkLit :: a -> Lit a sqlType :: Proxy a -> Text fromSql :: SqlValue -> a --- | An SQL mkLit.+-- | An SQL literal. data Lit a where LitS :: !Text -> Lit Text LitI :: !Int -> Lit Int@@ -99,16 +99,19 @@ sqlType _ = "INTEGER" fromSql (SqlInt x) = x fromSql v = error $ "fromSql: int column with non-int value: " ++ show v+ instance SqlType Double where mkLit = LitD sqlType _ = "DOUBLE" fromSql (SqlFloat x) = x fromSql v = error $ "fromSql: float column with non-float value: " ++ show v+ instance SqlType Text where mkLit = LitS sqlType _ = "TEXT" fromSql (SqlString x) = x fromSql v = error $ "fromSql: text column with non-text value: " ++ show v+ instance SqlType Bool where mkLit = LitB sqlType _ = "INT"@@ -116,6 +119,7 @@ fromSql (SqlInt 0) = False fromSql (SqlInt _) = True fromSql v = error $ "fromSql: bool column with non-bool value: " ++ show v+ instance SqlType UTCTime where mkLit = LitTS . pack . formatTime defaultTimeLocale sqlDateTimeFormat sqlType _ = "DATETIME"@@ -124,6 +128,7 @@ Just t -> t _ -> error $ "fromSql: bad datetime string: " ++ unpack s fromSql v = error $ "fromSql: datetime column with non-datetime value: " ++ show v+ instance SqlType Day where mkLit = LitDate . pack . formatTime defaultTimeLocale sqlDateFormat sqlType _ = "DATE"@@ -132,6 +137,7 @@ Just t -> t _ -> error $ "fromSql: bad date string: " ++ unpack s fromSql v = error $ "fromSql: date column with non-date value: " ++ show v+ instance SqlType TimeOfDay where mkLit = LitTime . pack . formatTime defaultTimeLocale sqlTimeFormat sqlType _ = "TIME"@@ -140,6 +146,7 @@ Just t -> t _ -> error $ "fromSql: bad time string: " ++ unpack s fromSql v = error $ "fromSql: time column with non-time value: " ++ show v+ instance SqlType a => SqlType (Maybe a) where mkLit (Just x) = LitJust $ mkLit x mkLit Nothing = LitNull
src/Database/Selda/Table.hs view
@@ -42,14 +42,6 @@ ComposeSpec ColSpec a b where ColSpec a +++ ColSpec b = ColSpec $ a ++ b --- | Insertion over all non-autoincrementing required columns.--- Autoincrementing primary keys are automatically assigned their value.-type family InsertCols a where- InsertCols (Auto a :*: b) = InsertCols b- InsertCols (a :*: Auto b) = a- InsertCols (a :*: b) = a :*: InsertCols b- InsertCols a = a- -- | A database table. -- Tables are parameterized over their column types. For instance, a table -- containing one string and one integer, in that order, would have the type@@ -92,10 +84,6 @@ ColSpec a ¤ ColSpec b = ColSpec (a ++ b) infixr 1 ¤ --- | Indicates an automatically incrementing column.--- Auto columns are usually not touched in @INSERT@ queries.-data Auto a- -- | Used by 'IsNullable' to indicate a nullable type. data Nullable @@ -107,12 +95,6 @@ IsNullable (Maybe a) = Nullable IsNullable a = NotNullable --- | Any table type that has an auto-incrementing primary key.-class HasAutoPrimary a-instance HasAutoPrimary (Auto a)-instance HasAutoPrimary (Auto a :*: b)-instance {-# OVERLAPPABLE #-} HasAutoPrimary b => HasAutoPrimary (a :*: b)- -- | Any SQL type which is NOT nullable. class SqlType a => NonNull a instance (SqlType a, IsNullable a ~ NotNullable) => NonNull a@@ -140,7 +122,7 @@ -- | Automatically increment the given attribute if not specified during insert. -- Also adds the @PRIMARY KEY@ attribute on the column.-autoPrimary :: ColName -> ColSpec (Auto Int)+autoPrimary :: ColName -> ColSpec Int autoPrimary n = ColSpec [c {colAttrs = [Primary, AutoIncrement, Required]}] where ColSpec [c] = newCol n :: ColSpec Int
src/Database/Selda/Table/Compile.hs view
@@ -2,6 +2,7 @@ -- | Generating SQL for creating and deleting tables. module Database.Selda.Table.Compile where import Database.Selda.Table+import Data.List (foldl') import Data.Monoid import Data.Text (Text, intercalate, pack) import qualified Data.Text as Text@@ -9,7 +10,7 @@ data OnError = Fail | Ignore deriving (Eq, Ord, Show) --- | Compile a @CREATAE TABLE@ query from a table definition.+-- | Compile a @CREATE TABLE@ query from a table definition. compileCreateTable :: (Text -> [ColAttr] -> Maybe Text) -> OnError -> Table a -> Text compileCreateTable customColType ifex tbl = mconcat [ "CREATE TABLE ", ifNotExists ifex, tableName tbl, "("@@ -39,22 +40,42 @@ -- | Compile an @INSERT INTO@ query inserting @m@ rows with @n@ cols each. -- Note that backends expect insertions to NOT have a semicolon at the end.-compInsert :: Table a -> Int -> Text-compInsert tbl mrows =- Text.unwords ["INSERT INTO", tableName tbl, names, "VALUES", vals]+compInsert :: Text -> Table a -> [[Bool]] -> Text+compInsert defaultKeyword tbl defs =+ Text.unwords ["INSERT INTO", tableName tbl, names, "VALUES", values] where- nonAutos =- [ colName c- | c <- tableCols tbl- , not (AutoIncrement `elem` colAttrs c)- ]- ncols = length nonAutos- names = "(" <> Text.intercalate ", " nonAutos <> ")"- cols n = "(" <> Text.intercalate ", " (mkParams ncols n) <> ")"- vals = Text.intercalate ", " $ zipWith (\f n -> f n)- (replicate mrows cols)- [0, ncols ..]- mkParams cs n = map (pack . ('$':) . show . (+n)) [1..cs]+ colNames = map colName $ tableCols tbl+ names = "(" <> Text.intercalate ", " colNames <> ")"+ values = Text.intercalate ", " (mkRows (1 :: Int) defs)++ -- Build all rows: just recurse over the list of defaults (which encodes+ -- the # of elements in total as well), building each row, keeping track+ -- of the next parameter identifier.+ mkRows n (ds:dss) =+ case mkRow n ds (tableCols tbl) of+ (n', vals) -> mkRowText (reverse vals) : mkRows n' dss+ mkRows _ _ =+ []++ mkRowText vals = "(" <> Text.intercalate ", " vals <> ")"++ -- Build a row: use the NULL/DEFAULT keyword for default rows, otherwise+ -- use a parameter.+ mkRow n ds cs = foldl' mkCols (n, []) (zip ds cs)++ -- Build a column: default values only available for for auto-incrementing+ -- primary keys.+ mkCol n def col+ | def && not (AutoIncrement `elem` colAttrs col) =+ error "only auto-incrementing primary keys may have defaults"+ | def =+ (n, defaultKeyword)+ | otherwise =+ (n+1, pack ('$':show n))++ -- Create a colum and return the next parameter id, plus the column itself.+ mkCols (n, cols) (def, col) =+ fmap (:cols) (mkCol n def col) -- | Compile a column attribute. compileColAttr :: ColAttr -> Text
src/Database/Selda/Transform.hs view
@@ -10,7 +10,9 @@ removeDeadCols :: [ColName] -> SQL -> SQL removeDeadCols live sql = case source sql' of+ EmptyTable -> sql' TableName _ -> sql'+ Values _ _ -> sql' Product qs -> sql' {source = Product $ map noDead qs} LeftJoin on l r -> sql' {source = LeftJoin on (noDead l) (noDead r)} where
src/Database/Selda/Types.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE GADTs, TypeOperators, TypeFamilies, FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Basic Selda types. module Database.Selda.Types where import Data.Text (Text)