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 |
From: Bjorn B. <bj...@br...> - 2008-03-24 10:57:37
|
Hi Justin, On Thu, Mar 20, 2008 at 12:06 AM, Justin Bailey <jgb...@gm...> wrote: > 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. Yes, that would be nice. > 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 Hmm, what if you want to apply this to a NOT NULL value? Should trim be overloaded? Or maybe there should be a some kind of lifting function to handle Maybes? > 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. Hmm, I get the feeling that this could be implemented in a simpler way. If we ignore the one-argument restriction on aggregates, couldn't you use the same trick as Text.Printf or the Remote class in HaXR (see http://darcs.haskell.org/haxr/Network/XmlRpc/Client.hs). The aggregate restriction could be handled by having a separate 'func' for aggregates, with a simpler type. Also, why are ExprNil and ExprCons needed? Couldn't func produce a PrimExpr internally? > 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 With the solution I refer to above, you should be able to write them as (not tested of course): trim str = func "trim" str rpad str len (Just char) = func "rpad" str (constant len) (constant char) rpad str len Nothing = func "rpad" str (constant len) every col = funcAggr "every" col /Bjorn |