viernes, diciembre 02, 2011

EasyMax: problemas de intercalacion (collation) en SQL Server

La librería FASTCALC.LIB utiliza frecuentemente una técnica que consiste en subir un cursor con ciertos datos al servidor en forma de tabla temporal, que luego es usada para filtrar datos específicos de una o mas tablas del sistema.

Sin embargo, cuando sucede que la BD utiliza un collation distinto al collation configurado en el servidor SQL Server (situación que se presenta típicamente cuando se restaura la BD desde un backup hecho en otro servidor que usaba un collation distinto), se produce un error de intercalación al intentar mezclar en un mismo SELECT los datos de la tabla temporal con datos de las tablas de la BD.

Aun peor, el problema no se limita a las funciones de FASTCALC.LIB sino que afecta a cualquier rutina que haga uso de las funciones QGetTempName(), QCursor2Temp() y QCloseTemp() de dicha librería.

El problema se genera en la función QVFP2ODBC(), que es la encargada de tomar un cursor de memoria y subirlo a la BD en forma de tabla temporal.  Al hacer esto, las columnas tipo CHAR o VARCHAR son creadas con el collation por omisión del servidor (digamos MODERN_SPANISH), el cual no necesariamente coincide con el collation usado en la BD actual (ej, LATIN_GENERAL).

La siguiente version de QVFP2ODBC() soluciona este problema instruyendo a SQL Server para que cree las columnas CHAR y VARCHAR de la tabla temporal usando el collation de la BD actual en lugar del collation del servidor, lo cual soluciona el problema al garantizar que tanto la tabla temporal como las tablas relacionadas en la BD actual usan el mismo collation:


* QVFP2ODBC (FASTCALC.LIB)
* Toma un cursor de memoria y lo sube a la BD actual utilizando el collation de la BD
*
#DEFPARAM pcCursor,pcTable,poDB

*-- Se obtienen la estructura del cursor local
*
#defvar aStr,nCount
#dim aStr[1x1]

#dbselect &pcCursor
nCount:=AFIELDS(aStr)

#let pcTable:="#"+pcCursor on vartype(pcTable)<>"C" or empty(pcTable)
#let pcTable:="#"+pcTable on left(pcTable,1)<>"#"
#let poDB:=CURDB on Type("poDB.Name")<>"C"


*-- Se crea el comando CREATE para crear la tabla temporal en SQL
*
#defvar i,cSQL,nResult,cSQLDT,cIL
cSQL:="CREATE TABLE "+pcTable+" ("
cIL:="("
#for i:=1 to nCount
 cSQLDT:=""
 #let cSQLDT:="CHAR("+ALLT(STR(aStr[i,3]))+") COLLATE database_default" on aStr[i,2]="C" and aStr[i,3]<=10
 #let cSQLDT:="VARCHAR("+ALLT(STR(aStr[i,3]))+") COLLATE database_default" on aStr[i,2]="C" and aStr[i,3]>10
 #let cSQLDT:="NUMERIC("+ALLT(STR(aStr[i,3]))+","+ALLT(STR(aStr[i,4]))+")" on aStr[i,2]="N"
 #let cSQLDT:="BIT" on aStr[i,2]="L"
 #let cSQLDT:="TEXT COLLATE database_default" on aStr[i,2]="M"
 #let cSQLDT:="DATETIME" on aStr[i,2] $ "DT"
 #let cSQLDT:="INT" ON aStr[i,2] $ "I"
 #IF EMPTY(cSQLDT)
  MESSAGEBOX("Tipo no reconocido: " + aStr[i,2],0,"QVFP2ODBC")
 #ENDIF
 cSQL:=cSQL + IIF(i>1,",","") + aStr[i,1] + " " + cSQLDT + " NOT NULL"
 cIL:=cIL + IIF(i>1,",","") + "?m."+aStr[i,1]
#endfor
cSQL:=cSQL + ")"
cIL:=cIL + ")"

#defvar nODBCHnd
nODBCHnd:=QGethODBC(poDB)

*-- Se crea la tabla temporal
nResult:=SQLEXEC(nODBCHnd,cSQL)
#IF nResult < 0
 SQLEXEC(nODBCHnd,"DROP TABLE "+pcTable)
 nResult:=SQLEXEC(nODBCHnd,cSQL)
#ENDIF
#If nResult < 0
 Kernel.Err.Capture()
 #return No
#ENDIF



*-- Se carga la data del cursor en la tabla temporal
*
#dbselect &pcCursor
#dbgo top
#dowhile not eof()
 #fieldstovars
 SQLEXEC(nODBCHnd,"INSERT INTO "+pcTable+" VALUES "+cIL)
 #dbskip
#ENDDO


#return Si