From: Justin B. <jgb...@gm...> - 2008-03-19 23:06:51
|
All, A feature haskelldb is lacking is a way to use SQL functions in queries which are not defined by the library. For example, the "trim" function is used frequently but can only be included in a haskelldb query through the 'literal' combinator or by writing your own function which creates the appropriate PrimExpr value. What I'd like to be able to do is use SQL functions in a type-safe way, just like those included in the library. For example, a trim function would look like: trim :: Expr (Maybe a) -> Expr (Maybe String) -- Nulls are still null To use the function in a project is as simple as: project $ rawNameField << customers ! Customers.name # name << trim (customers ! Customers.name) Some functions take multiple arguments. For example. 'rpad', which takes a column, a padding length, and an optional padding character. Notice not all arguments are expressions: rpad :: Expr a -> Expr Int -> Maybe String -> Expr (Maybe String) Sometimes the aggregate expressions provided by haskelldb aren't enough. The 'every' function is an aggregate found on postgresql: every :: Expr Bool -> ExprAggr Bool The code below allows these functions to be implemented in terms of two combinators - func and arg. It is intended to be included in the Query module, and only func and arg would exported. Questions I'd like answered: * Is the approach over-engineered? Is there a simpler way? * Is the feature useful? * Comments on the implementation? After the code the bodies of the functions above are given. -- Used to construct type-safe function definitions with -- arbitraryly typed exprssions. See func and arg to use these. -- The data and instances below are modeled on RecNil/RecCons from -- HDBRec. data ExprNil = ExprNil data ExprCons a b = ExprCons a b instance ToPrimExprs ExprNil where toPrimExprs ~ExprNil = [] instance (ExprC e, ToPrimExprs r) => ToPrimExprs (ExprCons (e a) r) where toPrimExprs ~(ExprCons e r) = primExpr e : toPrimExprs r class (ExprC e) => MakeFunc e r where {- | Combinator which can be used to define SQL functions which will appear in queries. Each argument for the function is specified by the arg combinator. All arguments must be chained together via function composition. Examples include: lower :: Expr a -> Expr (Maybe String) lower str = func "lower" $ arg str The arguments to the function do not have to be Expr if they can be converted to Expr: data DatePart = Day | Century deriving Show datePart :: DatePart -> Expr (Maybe CalendarTime) -> Expr (Maybe Int) datePart date col = func "date_part" $ arg (constant $ show date) . arg col Aggregate functions can also be defined: every :: Expr Bool -> ExprAggr Bool every col = func "every" $ arg col Note that type signatures are required on each function defined, as func is defined in a typeclass. Also, because of the implementation of aggregates, only one argument can be provided.-} func :: (ExprC e, ToPrimExprs r) => String -- | The name of the function -> (ExprNil -> ExprCons (Expr d) r) -- | The arguments, specified via arg combinators joined by composition. -> e o -- | The resulting expression. -- | This instance ensures only one argument can be provided to -- an aggregate function. instance MakeFunc ExprAggr ExprNil where func = funcA funcA :: String -> (ExprNil -> ExprCons (Expr a) ExprNil) -> ExprAggr o funcA name args = ExprAggr (AggrExpr (AggrOther name) (primExpr . unExprs $ (args ExprNil))) where unExprs :: ExprCons (Expr a) ExprNil -> Expr a unExprs ~(ExprCons e _) = e -- | This instance allows any number of expressions to be used as -- arguments to a non-aggregate function. instance MakeFunc Expr a where func = funcE funcE :: (ToPrimExprs r) => String -> (ExprNil -> ExprCons (Expr e) r) -> Expr o funcE name args = Expr (FunExpr name (toPrimExprs (args ExprNil))) -- | Used to specify an individual argument to a SQL function definition. This -- combinator must be strung together with function composition. That chain -- must be provided to the func combinator. arg :: (Expr a) -> (c -> ExprCons (Expr a) c) arg expr = ExprCons expr I modeled the code above after the (#) function and the RecCons/RecNil types from the HDBRec module. In this case, a "list" of expressions is built through composition with the arg combinator and handed to the func combinator. Depending on the result type (Expr or ExprAggr), one of the two instances is selected. Since ExprAggr can only take one argument, the instance is only defined for (ExprCons x ExprNil). A really ugly error will result if more than one argument is defined for an aggregrate. Non-aggregates, however, can take any number of arguments. The implementation of the motivating functions is then: trim str = func "trim" $ arg str rpad str len (Just char) = func "rpad" $ arg str . arg (constant len) . arg (constant char) rpad str len Nothing = func "rpad" $ arg str . arg (constant len) every col = func "every" $ arg col Thanks in advance to any responders! Justin |