ta-0.1: Database/TA/Core/GenSelect.hs
-- | 'GenSelect' . Leonel Fonseca, 2010.
-- Version 0.0.4
-- Adiciones:
-- Función 'ambBDCompatible' indica si el ambiente
-- en la base de datos produce código distinto
-- al generado durante la compilación.
-- Función 'verASTS' viejo nuevo muestra dos ASTs.
-- Versión 0.0.3
-- Cambios:
-- - empalmar recibe un parámetro que indicará
-- si el código resultante incluye meta-declaraciones.
-- - genSelect devuelve (comando relacional,generador)
-- versión 0.0.2
-- Énfasis en la generación de código con funciones de
-- biblioteca TH.
-- Énfasis en predefinir nombres locales de forma dinámica.
-- versión 0.0.1
-- Usa mezcla de quotation y funciones de la biblioteca TH
-- para construir declaraciones.
{-# options -fglasgow-exts #-}
{-# language TemplateHaskell #-}
{-# language PatternGuards #-}
{-# language TupleSections #-}
{-# language NoMonomorphismRestriction #-}
{-# language DisambiguateRecordFields #-}
{-# language StandaloneDeriving #-}
module Database.TA.Core.GenSelect
( empalmar
, genSelect
, ambBDCompatible
, verASTs
)
where
import Database.Oracle.Enumerator
import Data.Typeable
import Data.List ( intersperse )
import Data.Maybe ( fromMaybe )
import Data.Char ( toLower, toUpper )
import qualified Data.Map as Map
import Control.Monad
import Control.Monad.Trans (lift, liftIO)
import Language.Haskell.TH
import Database.TA.Helper.LiftQ
import Database.TA.Helper.TH ( pr, verAST )
import Database.TA.Helper.Text ( proper )
import qualified Database.TA.Core.MapaBase as MB
import qualified Database.TA.Core.RIS as RIS
import Database.TA.Core.RIS
import Database.TA.Core.Infraestructura
( describirConsulta
, collcatM
, exnAt
, concatM
, revertir )
import Database.TA.Core.Opciones ( imprimirDepuracion )
-- Para permitir la definición de empalmar.
deriving instance Typeable1 Q
{- |
En la versión 0.0.3 a la rutina 'empalmar' recibe un parámetro
para indicar si el valor Q [Dec] debe contener una
declaración de una variable cuyos contenidos son las
declaraciones de acceso a datos.
'empalmar' esquema constraseña servicio generadores
metaDescriptor
evalua la lista de generadores y acumula las citas
de declaraciones (Q [Dec]).
La evaluación de declaraciones ocurre en el esquema
indicado.
Si metaDescriptor = Nothing, no se incluye una variable
cuyo valor son las declaraciones.
Si metaDescriptor = Just nombre, las declaraciones retornadas
incluyen una variable "nombre" cuyo valor son las
declaraciones de acceso a la base de datos.
En el siguiente trozo, en la definición de empalmar:
En primera instancia queremos una definición como x.
let x = [d|oldCode=decs|]
Pero el nombre de las declaraciones siempre sería oldCode.
Esa ruta no sirve.
Tampoco sirve elaborar una declaración como
valD (varP mdN) (normalB $(metaLift decs)) []
porque 'decs' es un identificador ligado en el contexto.
Para que funcione, 'decs' debería ser importado.
Recuerdo de algún tutorial esta maña:
do mdN <- newName nombre
[ValD _ body _] <- [d|oldCode=decs|]
let md = ValD (VarP mdN) body []
liftM2 (++) decs md
donde se rescata el 'body' que interesa y se descarta
el identificador y la lista de declaraciones.
Enseguida, se contruye md, con el identificador
de nuestras simpatías.
-}
empalmar :: String -> String -> String -> Maybe String ->
( forall mk . [ DBM mk Session (Q [Dec]) ] ) -> Q [Dec]
empalmar usuario contraseña cxn metaDescriptor gs =
join $ runIO $ withSession (connect usuario contraseña cxn)
(do decs <- (collcatM gs)
return $ case metaDescriptor of
Just nombre ->
do [ValD _ body _] <- [d|oldCode=decs|]
let mdN = mkName nombre
md = return [ValD (VarP mdN) body []]
liftM2 (++) decs md
Nothing -> decs)
{- |
Genera las declaraciones de un tipo y una acción apropiados
para consultar la base de datos con la consulta especificada.
-}
genSelect :: CR -> DBM mark Session (Q [Dec])
genSelect (CR nombre pre suf op) =
case RIS.toString op of
"" -> error $ "No pudo generar una operación para "++ show op
opOk -> do
qcis <- describirConsulta opOk
let tipo = genTipoPS nombre pre suf qcis
select = genSelectWrkr nombre opOk qcis
qdecs = liftM2 (++) tipo select -- Declaraciones
when imprimirDepuracion
(do pr $ "\n* En genSelect: generación fresca para "
++ nombre ++ "*"
decs <- liftIO $ runQ qdecs
pr $ show decs
pr "\n")
return qdecs
-- | Genera un nombre para el tipo de datos del resultado
-- de una consulta.
genNombreTipo :: String -> Name
genNombreTipo [] = error $ "Error: En genNombreTipo necesita\
\ un nombre de consulta para derivar\
\ un nombre de tipo."
genNombreTipo x = mkName $ (toUpper $ head x) : (tail x)
{- |
Una versión que opera con información de un Select.
Genera el tipo del resultado de la consulta (estilo registro).
Requiere de:
- Un nombre para el tipo registro.
- Un prefijo para cada nombre de campo.
- Un sufijo para cada nombre de campo.
- Una lista de QueryColInfo con la información de una
sentencia SQL.
-}
genTipoPS :: String -> String -> String
-> GeneradorQueryColInfo ( Q [Dec] )
genTipoPS nomConsulta _ _ [] = return []
genTipoPS nomConsulta pre suf cols = do
d <- dataD contexto nombreTipo varTipos registro derivaciones
return [d]
where
contexto = cxt []
nombreTipo = genNombreTipo nomConsulta
varTipos = []
registro = [recC nombreTipo campos]
campos = map campo cols
campo :: QueryColInfo -> VarStrictTypeQ
campo qci =
varStrictType
( mkName $ map toLower $ pre ++ col_name qci ++ suf )
( strictType notStrict $ genQTipoCol qci )
derivaciones = map mkName ["Show","Read","Eq","Ord","Typeable"]
genQTipoCol :: QueryColInfo -> TypeQ
genQTipoCol qci =
if col_null_ok qci
then [t| Maybe $tipo |] -- Es nulificable.
else tipo -- No es nulificable.
where
tipo = case MB.dbTC2hs (col_type qci) (col_scale qci) of
Right tipo -> tipo
Left causa -> error $
"genQTipoCol no pudo generar un tipo para la columna "
++ (col_name qci) ++ " con tipo ("
++ show (col_type qci)
++ ") y escala " ++ show (col_scale qci)
++ " a causa de " ++ causa
-- | 'genNombres' recibe una hilera raiz y genera
-- n hileras con sufijos que van de 1 a n.
genNombres raiz n = map (mkName . (raiz++) . show) [1..n]
-- | 'ambBDCompatible' viejas generadorNuevas
--
-- 'viejas' es un conjunto de declaraciones generado
-- previamente.
-- 'generadorNuevas' es una lista de generadores
-- de declaraciones, como la que se construye cuando
-- se digita : [ genSelect $ CR "consulta"... $ "select..." ]
-- A partir del hecho que el generador de código es predecible
-- y estable, si las declaraciones antiguas no coinciden
-- con las frescas obtenidas por el generadorNuevas,
-- significa que hubo un cambio en el ambiente de bases de datos
-- y que es inseguro ejecutar el código.
ambBDCompatible :: Q [Dec]
-> [ DBM mark Session (Q [Dec]) ]
-> DBM mark Session (Bool,[Dec],[Dec])
ambBDCompatible viejas generadorNuevas = do
newCode <- collcatM generadorNuevas
newAST <- liftIO $ runQ newCode
oldAST <- liftIO $ runQ viejas
return $ (oldAST /= newAST, oldAST, newAST)
-- | 'verASTs' muestra a pantalla dos AST.
verASTs :: [Dec] -> [Dec] -> IO ()
verASTs v n = do separar
putStrLn $ show v
separar
putStrLn $ show n
separar
where
separar = putStrLn "" >> putStrLn linea >> putStrLn ""
linea = take 70 $ repeat '-'
{- Relevancia técnica alta.
Mezcla de estilo de citación:
(1) La declaración de XQ usa expresiones parentizadas.
incluye dos empalmes de expresiones, como
$(varE pSN), para capturar nombres que estarían
en el ambiente.
No se usa directamente el nombre 'ps' porque todavía
no existe.
Aquí mezcla el estilo parentizado con empalmes y
funciones de la biblioteca TH.
(3) La definición de gR.
Aquí mezcla el estilo parentizado con empalmes.
gR se define con funciones de la biblioteca TH,
cuyo cuerpo es definido con una citación,
donde se empalma la definición $cvtf.
(4) Una mezcla más ambiciosa (o enriquecida) donde
aparecen la declaración se define con reiteraciones
de expresiones parentizadas y funciones de la biblioteca
TH. Dos ejemplos: 'topBody' y 'pS'.
En la definición de 'cvtf', 'appsE' hace un foldl entre
el primer elemento de la lista y los elementos subsiguientes.
En el módulo TAGenTipo hay una versión para Typ.
La definición de 'bd' de 'ite', es equivalente al uso de
la citación de nombre:
bd = appE (varE 'result')
(infixE (Just $ tupE vars)
[|(:)|]
(Just $ varE $ last nom))
-}
{-
qCosa bindingsList = concatM xQ bindingsList `exnAt` "qCosa: "
where
ite a b acc = result' ( (a,b):acc )
gR gRbnds = liftM ( revertir ( \(a,b) -> (QCosa a b) ) )
( doQuery gRbnds ite [] )
pS = prepareQuery $ sql $ "select :1 hilera, :2 x from dual"
xQ bindings = withPreparedStatement pS
(\bs -> withBoundStatement bs bindings gR)
-}
genSelectWrkr :: Nombre -> String -> [QueryColInfo] -> Q [Dec]
genSelectWrkr nomConsulta sqlText cols = do
qName <- newName topN
d1 <- iteD -- Dec <- DecQ Recolección de declaraciones.
d2 <- gRD -- Dec <- DecQ
d3 <- pSD -- Dec <- Q [Dec]
d4 <- xQD -- Dec <- DecQ
let ds1 = [d1] ++ [d2] ++ d3 ++ [d4] -- [Dec]
ds2 = map return ds1 -- [DecQ]
top <- funD (mkName topN) [clause [(varP $ mkName "bnds")]
(normalB topBody ) ds2]
return [top]
where
topBody = [| concatM $(dyn "xQ") $(varE $ mkName "bnds")
`exnAt` rutina
|]
k = length cols -- La cantidad de columnas.
-- Preparamos identificadores.
-- garantizamos que el nombre de la acción de
-- consulta tiene la primer letra minúscula.
topN = (toLower $ head nomConsulta) : (tail nomConsulta)
rutina = topN ++ ": "
iteN = mkName "ite" -- nombre de función de iteración
gRN = mkName "gR" -- función getResults
pSN = mkName "pS" -- función prepareStatement
xQN = mkName "xQ" -- función executeQuery
iteD :: DecQ
iteD = funD iteN [ clause ps (normalB bd) [] ]
where
nom = genNombres "a1" (k+1) -- incluye uno para acumular.
ps = map varP nom -- [VarP a1,VarP a2,VarP aN].
vars = map varE (take k nom) -- excluimos el acumulador.
bd = [|result' ( $(tupE vars) : $(varE $ last nom) )|]
gRD :: DecQ
gRD = funD gRN [ clause [varP bnsN] (normalB bd) [] ]
where
bnsN = mkName "gRbnds" -- parárametro "bindings" en "gR"
nom = genNombres "a2" k -- otra tanda de nombres.
ps = map varP nom -- [VarP a1,VarP a2,VarP aN].
vars = map varE nom -- otra tanda de variables.
cvtf = lamE [tupP ps] $ appsE ((conE nomT):vars)
nomT = genNombreTipo nomConsulta
bd = [|liftM (revertir $cvtf)
(doQuery $(varE bnsN) $(varE iteN) []) |]
pSD :: Q [Dec] -- esta declaración es de tipo ValD.
pSD = [d|pS = prepareQuery $ sql sqlText |]
xQD :: DecQ
xQD = funD xQN [ clause [varP bnsN] (normalB bd) [] ]
where
bnsN = mkName "xQbnds" -- parámetro "bindings" en "xQ"
bSTN = mkName "boundSt" -- nombre boundStatement
bd0 = [| withPreparedStatement $(varE pSN) (\bs ->
withBoundStatement bs $(varE bnsN) $(varE gRN))
|]
bd = [| withPreparedStatement $(varE pSN) $bd2 |]
bd2 = lamE [varP bSTN]
(appE (appE (appE (varE 'withBoundStatement)
(varE bSTN))
(varE bnsN))
(varE gRN))
{-
El modelo de generación: Cada comentario numerado
indica una sub-rutina de generación necesaria.
{-1-} es provisto por genTipoPS
{-8-} es la hilera (RIS.toString (op comandoRelacional))
En genSelectITE, hay subrutinas
ite -> {-2-},
cvtf -> {-2b-},
gR -> {-3-},
pS -> {-4-},
xQ -> {-5-} .
{-1-}
data QCosa :: QCosa {f1 :: tCol1, f2 :: tCol2, .. , fn :: tColn)
deriving (Read, Show, Eq, Ord, Typeable)
{-7-}
qCosa bindings =
concatM xQ bindings `exnAt` "qCosa: "
where {-6-}
{-2-}
ite a b acc = result' ( (a,b):acc )
{-3-}
gR bs = liftM ( revertir
{-2b-} ( \(a,b) -> (QCosa a b) )
)
( doQuery bs ite [] )
{-4-}
ps = prepareQuery $
sql $ {-8-}
"select :1 hilera, length(:1) largo from dual"
{-5-}
xQ b = withPreparedStatement ps (\bs->withBoundStatement bs b gR)
-}
{-
Si se siente desolado por las abreviaturas, las explico:
ite = iterador
gR = getResults
ps = preparedStatement
bs = boundStatement
xQ = executeQuery (bound variables and retrieve results)
-}
{-
Método de trabajo:
Establezca modelo
Escriba declaración 'x' con quotes en TATTest01.hs.
Cargue TATTest01.hs en GHCi.
Obtenga AST del modelo via Aux01.verCodigo x.
Ahora valide iterativamente:
Agruegue (o modifique) una declaración 'y'
con sintaxis TH en TATTest01.hs.
Recargue en GHCi.
Si coincide el AST de la declaración 'x'
con el AST de la declaración 'y', adopte el generador
de código, por ejemplo, en TAGenSelect.hs.
-}
{-
Podemos generar sentencias S correctas a partir del tipo T
que tenga una consulta O. Las sentencias S son inyectadas
al proceso de compilación y se convierten código objeto.
Sin embargo si cambia la forma de la tabla o vista que sirven
de base para la consulta, las sentencias S estarán incorrectas
y no lo sabremos.
¿Qué tal si rechequeamos todo?
Para ello necesitamos guardar S y O. Luego generar
una acción 'comprobarAmbiente' que haga lo siguiente:
- Genera S' a partir de O invocando genSelect
en el ambiente de ejecución actual.
- Compara S' con S.
- Si son distintos, habrá un error porque
el ambiente actual es distinto del original
y ocurrirán errores de ejecución.
- Si son iguales, el ambiente actual es lo
suficiente similar al original para ejecutar
las sentencias compiladas.
- Para poder invocar genSelect es necesario invocar
el compilador GHC en tiempo de ejecución.
-}
{-
Equivalencias de código
dyn "x" == varE $ mkName "x"
bd0 = [| withPreparedStatement $(varE pSN) (\bs ->
withBoundStatement bs $(varE bnsN) $(varE gRN))
|]
==
bd = [| withPreparedStatement $(varE pSN) $bd2 |]
bd2 = lamE [varP bSTN]
(appE (appE (appE (varE 'withBoundStatement)
(varE bSTN))
(varE bnsN))
(varE gRN))
-}
{- Generación de código inseguro.
Usar mkName "x" posibilita enlace dinámico. La rutina con
nombre "x" más cercana en el árbol de llamadas será invocada.
Sin embargo, aquí no tiene esos efectos colaterales porque
el ambiente más cercano está cerrado: son subrutinas.
Es necesario operar con mkName y no con newName. mkName
facilita genera nombres repetidos siempres. newName no
genera el mismo nombre. Si queremos comparar código en
generado en momentos distintos, necesitamos la predictibilidad
de mkName.
Este fragmento de código en uf04.hs muestra la insensibilidad
a la inclusión dinámica de identificadores.
El código opera bien.
r <- withSession s $ do
let gR = \_ -> [(Just 1, Just "uno"), (Just 2,Just "dos")]
pr (show $ gR id)
qDosColumnas [[]]
-}