Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: b4ad75a13c
Fetching contributors…

Cannot retrieve contributors at this time

418 lines (380 sloc) 21.159 kb
module Turbinado.Database.ORM.Output where
import qualified Data.Char
import Control.Monad
import Data.Dynamic
import qualified Data.Map as M
import Data.Maybe
import Data.List
import Database.HDBC
import System.Directory
import System.FilePath
import Turbinado.Database.ORM.Types
type TypeName = String
-- TODO: This file needs to be completely torn up and generalized.
writeModels ts =
do writeFile "App/Models/Bases/Common.hs" generateCommon
mapM_ (\(t, (cs, pk)) ->
let typeName = (toType t) in
do e <- doesFileExist (joinPath ["App/Models", typeName ++ ".hs"])
when (not e) (writeFile (joinPath ["App/Models", typeName++".hs"]) (generateModelFile typeName) )
writeFile (joinPath ["App/Models/Bases", typeName ++ "Type.hs"]) (generateType t typeName pk ts cs)
writeFile (joinPath ["App/Models/Bases", typeName ++ "Functions.hs"]) (generateFunctions t typeName pk ts cs)
writeFile (joinPath ["App/Models/Bases", typeName ++ "Relations.hs"]) (generateRelations t typeName pk ts cs)
) $ M.toList ts
---------------------------------------------------------------------------
-- File templates --
---------------------------------------------------------------------------
generateType :: TableName ->
TypeName ->
PrimaryKey ->
Tables ->
Columns ->
String
generateType t typeName pk ts cs =
unlines $
["{- DO NOT EDIT THIS FILE"
," THIS FILE IS AUTOMAGICALLY GENERATED AND YOUR CHANGES WILL BE EATEN BY THE GENERATOR OVERLORD"
,""
," All changes should go into the Model file (e.g. App/Models/ExampleModel.hs)"
,"-}"
,""
,"module App.Models.Bases." ++ typeName ++ "Type where"
, ""
, "import App.Models.Bases.Common"
, "import Data.Maybe"
, "import System.Time"
, ""
] ++
["-- The data type for this model"] ++
[ "data " ++ typeName ++ " = " ++ typeName ++ " {"
] ++
[intercalate ",\n" (map columnToFieldLabel (M.toList cs))] ++
[ " } deriving (Eq, Show, Typeable)"
, ""
, "instance DatabaseModel " ++ typeName ++ " where"
, " tableName _ = \"" ++ t ++ "\""
, ""
]
generateFunctions :: TableName ->
TypeName ->
PrimaryKey ->
Tables ->
Columns ->
String
generateFunctions t typeName pk ts cs =
unlines $
["{- DO NOT EDIT THIS FILE"
," THIS FILE IS AUTOMAGICALLY GENERATED AND YOUR CHANGES WILL BE EATEN BY THE GENERATOR OVERLORD"
,""
," All changes should go into the Model file (e.g. App/Models/ExampleModel.hs)"
,"-}"
,""
,"module App.Models.Bases." ++ typeName ++ "Functions where"
, ""
, "import App.Models.Bases.Common"
, "import qualified Database.HDBC as HDBC"
, "import Data.Maybe"
, "import System.Time"
, ""
, " -- My type"
, "import App.Models.Bases." ++ typeName ++ "Type"
, ""
, "import Turbinado.Environment.Types"
, "import Turbinado.Environment.Database"
, ""
] ++
[""] ++
generateHasFindByPrimaryKey t cs typeName pk ++
[""] ++
generateIsModel t cs typeName
++
[""
,"deleteWhere :: (HasEnvironment m) => SelectString -> SelectParams -> m Integer"
,"deleteWhere ss sp = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.run conn (\"DELETE FROM " ++ t++ " WHERE (\" ++ ss ++ \") \") sp"
," return res"
]
generateRelations :: TableName ->
TypeName ->
PrimaryKey ->
Tables ->
Columns ->
String
generateRelations t typeName pk ts cs =
unlines $
["{- DO NOT EDIT THIS FILE"
," THIS FILE IS AUTOMAGICALLY GENERATED AND YOUR CHANGES WILL BE EATEN BY THE GENERATOR OVERLORD"
,""
," All changes should go into the Model file (e.g. App/Models/ExampleModel.hs)"
,"-}"
,""
,"module App.Models.Bases." ++ typeName ++ "Relations where"
, ""
, "import App.Models.Bases.Common"
, "import qualified Database.HDBC as HDBC"
, "import Data.Maybe"
, "import System.Time"
, ""
, " -- Model imports"
, "import App.Models.Bases." ++ typeName ++ "Type"
, unlines $ generateChildModelImports cs
, unlines $ generateParentModelImports t ts
, ""
, "import Turbinado.Environment.Types"
, "import Turbinado.Environment.Database"
, ""
] ++
[""] ++
[""] ++
generateHasChildren t cs typeName ++
[""] ++
[""] ++
generateHasParents t ts
generateChildModelImports cs =
map (\ctn -> "import qualified App.Models.Bases." ++ toType ctn ++ "Type as " ++ toType ctn ++ "Type\nimport qualified App.Models.Bases." ++ toType ctn ++ "Functions as " ++ toType ctn ++ "Functions") $
nub $
map fst $ concat $
map (\(_, fks, _) -> fks) $ M.elems cs
generateParentModelImports t ts =
map (\ptn -> "import qualified App.Models.Bases." ++ toType ptn ++ "Type as " ++ toType ptn ++ "Type\nimport qualified App.Models.Bases." ++ toType ptn ++ "Functions as " ++ toType ptn ++ "Functions") $
nub $ filter (not . null) $
map parentFilter $ M.assocs ts
where parentFilter (ptn, (cs, _)) =
case (filter (\(tn, _) -> t == tn) $ concat $ map (\(_, fks, _) -> fks) $ M.elems cs) of
[] -> []
_ -> ptn
generateModelFile typeName =
unlines $
["module App.Models." ++ typeName
," ( module App.Models." ++ typeName
," , module App.Models.Bases." ++ typeName ++ "Type"
," , module App.Models.Bases." ++ typeName ++ "Functions"
," , module App.Models.Bases." ++ typeName ++ "Relations"
," , module App.Models.Bases.Common"
," ) where"
,"import App.Models.Bases." ++ typeName ++ "Type"
,"import App.Models.Bases." ++ typeName ++ "Functions"
,"import App.Models.Bases." ++ typeName ++ "Relations"
,"import App.Models.Bases.Common"
]
generateCommon:: String
generateCommon = unlines $
["{- DO NOT EDIT THIS FILE"
," THIS FILE IS AUTOMAGICALLY GENERATED AND YOUR CHANGES WILL BE EATEN BY THE GENERATOR OVERLORD -}"
,""
,"module App.Models.Bases.Common("
," module App.Models.Bases.Common,"
," module Control.Exception,"
," module Control.Monad.Trans,"
," module Data.Int"
," ) where"
,""
,"import Control.Monad.Trans"
,"import Control.Exception"
,"import Database.HDBC"
,"import Data.Int"
,""
,"import Turbinado.Environment.Types"
,""
,"-- Using phantom types here "
,"class DatabaseModel m where"
," tableName :: m -> String"
,""
,"type SelectString = String"
,"type SelectParams = [SqlValue]"
,"type OrderByParams = String"
,""
,"class (DatabaseModel model) =>"
," IsModel model where"
," insert :: (HasEnvironment m) => model -> Bool -> m (Maybe Integer)"
," findAll :: (HasEnvironment m) => m [model]"
," findAllWhere :: (HasEnvironment m) => SelectString -> SelectParams -> m [model]"
," findAllOrderBy :: (HasEnvironment m) => OrderByParams -> m [model]"
," findAllWhereOrderBy :: (HasEnvironment m) => SelectString -> SelectParams -> OrderByParams -> m [model]"
," findOneWhere :: (HasEnvironment m) => SelectString -> SelectParams -> m model"
," findOneOrderBy :: (HasEnvironment m) => OrderByParams -> m model"
," findOneWhereOrderBy :: (HasEnvironment m) => SelectString -> SelectParams -> OrderByParams -> m model"
,""
,"class (DatabaseModel model) =>"
," HasFindByPrimaryKey model primaryKey | model -> primaryKey where"
," find :: (HasEnvironment m) => primaryKey -> m model"
," delete :: (HasEnvironment m) => primaryKey -> m ()"
," update :: (HasEnvironment m) => model -> m ()"
,""
]
---------------------------------------------------------------------------
-- Generator templates --
---------------------------------------------------------------------------
generateIsModel :: TableName -> Columns -> TypeName -> [String]
generateIsModel t cs typeName =
["instance IsModel " ++ typeName ++ " where"
," insert m returnId = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.run conn (\" INSERT INTO " ++ t ++ " (" ++ (intercalate "," $ M.keys cs) ++") VALUES (" ++ (intercalate "," $ map generateQs (M.assocs cs) ) ++ ")\") ( " ++ (intercalate " ++ " $ filter (not . null) $ map generateArgs (M.assocs cs) ) ++ ")"
," case res of"
," 0 -> (liftIO $ HDBC.handleSqlError $ HDBC.rollback conn) >>"
," (throwDyn $ HDBC.SqlError"
," {HDBC.seState = \"\","
," HDBC.seNativeError = (-1),"
," HDBC.seErrorMsg = \"Rolling back. No record inserted :" ++ t ++ " : \" ++ (show m)"
," })"
," 1 -> liftIO $ HDBC.handleSqlError $ HDBC.commit conn >>"
," if returnId"
," then do i <- liftIO $ HDBC.catchSql (HDBC.handleSqlError $ HDBC.quickQuery' conn \"SELECT lastval()\" []) (\\_ -> HDBC.commit conn >> (return $ [[HDBC.toSql (0 :: Integer)]]) ) "
," return $ HDBC.fromSql $ head $ head i"
," else return Nothing"
," findAll = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn \"SELECT " ++ cols cs ++ " FROM " ++ t ++ "\" []"
," return $ map (\\r -> " ++ generateConstructor cs typeName ++ ") res"
," findAllWhere ss sp = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " WHERE (\" ++ ss ++ \") \") sp"
," return $ map (\\r -> " ++ generateConstructor cs typeName ++ ") res"
," findAllOrderBy op = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " ORDER BY \" ++ op) []"
," return $ map (\\r -> " ++ generateConstructor cs typeName ++ ") res"
," findAllWhereOrderBy ss sp op = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " WHERE (\" ++ ss ++ \") ORDER BY \" ++ op) sp"
," return $ map (\\r -> " ++ generateConstructor cs typeName ++ ") res"
," findOneWhere ss sp = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " WHERE (\" ++ ss ++ \") LIMIT 1\") sp"
," return $ (\\r -> " ++ generateConstructor cs typeName ++ ") (head res)"
," findOneOrderBy op = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " ORDER BY \" ++ op ++ \" LIMIT 1\") []"
," return $ (\\r -> " ++ generateConstructor cs typeName ++ ") (head res)"
," findOneWhereOrderBy ss sp op = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t++ " WHERE (\" ++ ss ++ \") ORDER BY \" ++ op ++\" LIMIT 1\") sp"
," return $ (\\r -> " ++ generateConstructor cs typeName ++ ") (head res)"
]
where generateQs :: (String, (SqlColDesc, ForeignKeyReferences, HasDefault)) -> String
generateQs (c, (desc, _, False)) = if ((colNullable desc) == Just True) then ("\" ++ (case (" ++ toFunction c ++ " m) of Nothing -> \"DEFAULT\"; Just x -> \"?\") ++ \"") else "?"
generateQs (c, (_, _, True)) = "\" ++ (case (" ++ toFunction c ++ " m) of Nothing -> \"DEFAULT\"; Just x -> \"?\") ++ \""
generateArgs :: (String, (SqlColDesc, ForeignKeyReferences, HasDefault)) -> String
generateArgs (c, (desc, _, False)) = if ((colNullable desc) == Just True) then ("(case (" ++ toFunction c ++ " m) of Nothing -> []; Just x -> [HDBC.toSql x])") else ("[HDBC.toSql $ " ++ toFunction c ++ " m]")
generateArgs (c, (_, _, True)) = "(case (" ++ toFunction c ++ " m) of Nothing -> []; Just x -> [HDBC.toSql x])"
generateHasFindByPrimaryKey :: TableName -> Columns -> TypeName -> PrimaryKey -> [String]
generateHasFindByPrimaryKey t cs typeName pk =
case (length pk) of
0 -> [""]
_ -> ["instance HasFindByPrimaryKey " ++ typeName ++ " " ++ " (" ++ unwords (intersperse "," (map (\c -> getHaskellTypeString $ colType $ (\(c',_,_) -> c') $ fromJust $ M.lookup c cs) pk)) ++ ") " ++ " where"
," find pk@(" ++ (concat $ intersperse ", " $ map (\i -> "pk"++(show i)) [1..(length pk)]) ++ ") = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn (\"SELECT " ++ cols cs ++ " FROM " ++ t ++ " WHERE (" ++ (generatePrimaryKeyWhere pk) ++ ")\") [" ++ (unwords $ intersperse "," $ map (\(c,i) -> "HDBC.toSql pk" ++ (show i)) (zip pk [1..])) ++ "]"
," case res of"
," [] -> throwDyn $ HDBC.SqlError"
," {HDBC.seState = \"\","
," HDBC.seNativeError = (-1),"
," HDBC.seErrorMsg = \"No record found when finding by Primary Key:" ++ t ++ " : \" ++ (show pk)"
," }"
," r:[] -> return $ " ++ (generateConstructor cs typeName)
," _ -> throwDyn $ HDBC.SqlError"
," {HDBC.seState = \"\","
," HDBC.seNativeError = (-1),"
," HDBC.seErrorMsg = \"Too many records found when finding by Primary Key:" ++ t ++ " : \" ++ (show pk)"
," }"
,""
," delete pk@(" ++ (concat $ intersperse ", " $ map (\i -> "pk"++(show i)) [1..(length pk)]) ++ ") = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.run conn (\"DELETE FROM " ++ t ++ " WHERE (" ++ (generatePrimaryKeyWhere pk) ++ ")\") [" ++ (unwords $ intersperse "," $ map (\(c,i) -> "HDBC.toSql pk" ++ (show i)) (zip pk [1..])) ++ "]"
," case res of"
," 0 -> (liftIO $ HDBC.handleSqlError $ HDBC.rollback conn) >>"
," (throwDyn $ HDBC.SqlError"
," {HDBC.seState = \"\","
," HDBC.seNativeError = (-1),"
," HDBC.seErrorMsg = \"Rolling back. No record found when deleting by Primary Key:" ++ t ++ " : \" ++ (show pk)"
," })"
," 1 -> (liftIO $ HDBC.handleSqlError $ HDBC.commit conn) >> return ()"
," _ -> (liftIO $ HDBC.handleSqlError $ HDBC.rollback conn) >>"
," (throwDyn $ HDBC.SqlError"
," {HDBC.seState = \"\","
," HDBC.seNativeError = (-1),"
," HDBC.seErrorMsg = \"Rolling back. Too many records deleted when deleting by Primary Key:" ++ t ++ " : \" ++ (show pk)"
," })"
,""
," update m = do"
," conn <- getEnvironment >>= (return . fromJust . getDatabase )"
," res <- liftIO $ HDBC.handleSqlError $ HDBC.run conn \"UPDATE " ++ t ++ " SET (" ++ (unwords $ intersperse "," $ M.keys cs) ++ ") = (" ++ (intercalate "," $ (take (M.size cs) (repeat "?"))) ++ ") WHERE (" ++ (generatePrimaryKeyWhere pk) ++")\""
," [" ++ (unwords $ intersperse "," $ map (\c -> "HDBC.toSql $ " ++ toFunction c ++ " m") (M.keys cs) ) ++ ", " ++ (unwords $ intersperse "," $ map (\c -> "HDBC.toSql $ " ++ toFunction c ++ " m") pk ) ++ "]"
," liftIO $ HDBC.handleSqlError $ HDBC.commit conn"
," return ()"
]
generateHasChildren :: TableName -> Columns -> TypeName -> [String]
generateHasChildren t cs typeName = map (\(cn, cd) -> generateHasChildren_t t cn cd typeName) $ M.assocs cs
generateHasChildren_t :: TableName -> ColumnName -> ColumnDesc -> TypeName -> String
generateHasChildren_t t cn (_, fks, _) typeName = unlines $ map (\(fkt, fkc) -> generateHasChildren_t_k t cn fkt fkc typeName) fks
generateHasChildren_t_k :: TableName -> ColumnName -> TableName -> ColumnName -> TypeName -> String
generateHasChildren_t_k t cn fkt fkc typeName =
unlines $
["findAllChild" ++ toType fkt ++ " :: (HasEnvironment m) => " ++ toType t ++ " -> m [" ++ toType fkt ++ "Type." ++ toType fkt ++ "]"
,"findAllChild" ++ toType fkt ++ " p = findAllWhere \"" ++ fkc ++ " = ?\" [HDBC.toSql $ " ++ toFunction cn ++ " p]"
]
generateHasParents :: TableName -> Tables -> [String]
generateHasParents ctn ts =
map (\(tname, cname, ptname, pcname) -> generateHasParent_t tname cname ptname pcname) $
nub $ concat $
map parentFilter $ M.assocs ts
where parentFilter (tn, (cs', _)) = filter (\(_, _, tn', _) -> ctn == tn') $ concat $ map (\(cn, (_, fks, _)) -> map (\(ptn, pcn) -> (tn, cn, ptn, pcn)) fks) $ M.assocs cs'
generateHasParent_t :: TableName -> ColumnName -> TableName -> ColumnName -> String
generateHasParent_t ptn pcn ctn ccn =
unlines $
["parent" ++ toType ptn ++ " :: (HasEnvironment m) => " ++ toType ctn ++ " -> m " ++ toType ptn ++ "Type." ++ toType ptn
,"parent" ++ toType ptn ++ " self = findOneWhere \"" ++ pcn ++ " = ?\" [HDBC.toSql $ " ++ toFunction ccn ++ " self]"
]
{-----------------------------------------------------------------------}
generatePrimaryKeyWhere pk =
unwords $
intersperse "++ \" AND \" ++ \"" $
map (\(c,i) -> c ++ " = ? ") (zip pk [1..])
generateConstructor cs typeName =
typeName ++ " " ++ (unwords $
map (\i -> "(HDBC.fromSql (r !! " ++ (show i) ++ "))") [0..((M.size cs) - 1)])
---------------------------------------------------------------------------
-- Utility functions --
---------------------------------------------------------------------------
cols :: Columns -> String
cols cs = unwords $ intersperse "," $ M.keys cs
columnToFieldLabel :: (String, (SqlColDesc, ForeignKeyReferences, HasDefault)) -> String
columnToFieldLabel cd@(name, (desc, _, _)) =
" " ++ toFunction name ++ " :: " ++
maybeColumnLabel cd ++
getHaskellTypeString (colType desc)
maybeColumnLabel :: (String, (SqlColDesc, ForeignKeyReferences, HasDefault)) -> String
maybeColumnLabel (_, (_, _, True)) = "Maybe " -- Does the column have a default
maybeColumnLabel (_, (desc, _, _)) = if ((colNullable desc) == Just True) then "Maybe " else ""
getHaskellTypeString :: SqlTypeId -> String
getHaskellTypeString SqlCharT = "String"
getHaskellTypeString SqlVarCharT = "String"
getHaskellTypeString SqlLongVarCharT = "String"
getHaskellTypeString SqlWCharT = "String"
getHaskellTypeString SqlWVarCharT = "String"
getHaskellTypeString SqlWLongVarCharT = "String"
getHaskellTypeString SqlDecimalT = "Rational"
getHaskellTypeString SqlNumericT = "Rational"
getHaskellTypeString SqlSmallIntT ="Int32"
getHaskellTypeString SqlIntegerT = "Int32"
getHaskellTypeString SqlRealT = "Rational"
getHaskellTypeString SqlFloatT = "Float"
getHaskellTypeString SqlDoubleT = "Double"
getHaskellTypeString SqlTinyIntT = "Int32"
getHaskellTypeString SqlBigIntT = "Int64"
getHaskellTypeString SqlDateT = "ClockTime"
getHaskellTypeString SqlTimeT = "ClockTime"
getHaskellTypeString SqlTimestampT = "ClockTime"
getHaskellTypeString SqlUTCDateTimeT = "ClockTime"
getHaskellTypeString SqlUTCTimeT = "TimeDiff"
getHaskellTypeString _ = error "Don't know how to translate this SqlTypeId to a SqlValue"
-- | Used for safety. Lowercases the first letter to
-- make a valid function.
toFunction [] = error "toFunction passed an empty string"
toFunction (firstLetter:letters) = (Data.Char.toLower firstLetter) : letters
-- | Used for safety. Uppercases the first letter to
-- make a valid type.
toType [] = error "toType passed an empty string"
toType (firstLetter:letters) = (Data.Char.toUpper firstLetter) : letters
Jump to Line
Something went wrong with that request. Please try again.