packages feed

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 [[]]  



-}