пятница, 9 октября 2009 г.

Решение проблемы с типизацией переменных при биндинге в Tclsqlite

Выполняем скрипт с нижеприведенным кодом:
$ cat /tmp/test
package require sqlite3
sqlite3 db :memory:

db eval {create table test(a int);insert into test values (1);}
proc test {label sql result} {
global i j
puts -nonewline $label\t
set _result [db eval $sql]
if { $_result eq $result} {
puts OK
} else {
puts ERROR\t$result!=$_result
}
}
set i 1

test 1.0 {select typeof($i)} integer ;# it doesn't work in orig sqlite
test 1.1 {select * from test where a=$i} 1
test 1.2 {select * from test where 1=$i} 1 ;# it doesn't work in orig sqlite
test 1.3 {select a from test where a IN (cast($i AS INT), 160)} 1
test 1.4 {select a from test where 1 IN (cast($i AS INT), 160)} 1


$ tclsh8.5 /tmp/test
1.0 ERROR integer!=text
1.1 OK
1.2 ERROR 1!=
1.3 OK
1.4 OK


А теперь тот же самый код запускаем в tclsh8.5 шелле:
$ tclsh8.5
% package require sqlite3
sqlite3 db :memory:

db eval {create table test(a int);insert into test values (1);}
proc test {label sql result} {
global i j
puts -nonewline $label\t
set _result [db eval $sql]
if { $_result eq $result} {
puts OK
} else {
puts ERROR\t$result!=$_result
}
}
set i 1

test 1.0 {select typeof($i)} integer ;# it doesn't work in orig sqlite
test 1.1 {select * from test where a=$i} 1
test 1.2 {select * from test where 1=$i} 1 ;# it doesn't work in orig sqlite
test 1.3 {select a from test where a IN (cast($i AS INT), 160)} 1
test 1.4 {select a from test where 1 IN (cast($i AS INT), 160)} 1

3.6.18
% % % % % % % 1
% 1
% % 1.0 OK
% 1.1 OK
% 1.2 OK
% 1.3 OK
% 1.4 OK
% %


Результат выполнения, как видим, отличается. Это не единственный баг, возникающий из-за некорректного биндинга тиклевых переменных, еще есть баг с проверкой типа вставляемых данных в констрэйнтах на таблицу, а также баг с некорректным типом результата выполнения тиклевой функции из sql-запроса и другие. Собственно, само решение простое - сделать "честную" типизацию, проверяя, может ли переменная иметь числовое представление, вместо того, чтобы ограничиваться проверкой, имеет ли переменная уже такое представление - в тикле это отнюдь не одно и то же.

В качестве обходного маневра при работе с оригинальным sqlite можно делать следующее:


package require sqlite3
sqlite3 db :memory:

set i 1
puts [db onecolumn {select typeof($i)}]
string is wideint $i
puts [db onecolumn {select typeof($i)}]

set i 1.1
puts [db onecolumn {select typeof($i)}]
string is double $i
puts [db onecolumn {select typeof($i)}]


Результатом выполнения из скрипта будет
text
integer
text
real


А из tclsh шелла получим вот что
integer
integer
real
real


После вызова "string is wideint" переменная имеет числовое представление, если это возможно. Понятно, если мы не знаем тип переменной, то нужно приводить к double и wideint, именно в таком порядке. Получается программирование на побочных эффектах.

В апстрим я уже несколько багрепортов отправлял, пока безрезультатно. В своей сборке этот баг правлю.

--- tclsqlite.c.old     2009-09-05 00:37:43.000000000 +0400                                           
+++ tclsqlite.c 2009-10-09 02:50:39.000000000 +0400
@@ -754,26 +754,18 @@
}else{
Tcl_Obj *pVar = Tcl_GetObjResult(p->interp);
int n;
+ Tcl_WideInt v;
+ double r;
u8 *data;
- char *zType = pVar->typePtr ? pVar->typePtr->name : "";
- char c = zType[0];
- if( c=='b' && strcmp(zType,"bytearray")==0 && pVar->bytes==0 ){
+ if( pVar->typePtr && pVar->typePtr->name[0]=='b' && strcmp(pVar->typePtr->name,"bytearray")==0 && pVar->bytes==0 ){
/* Only return a BLOB type if the Tcl variable is a bytearray and
** has no string representation. */
data = Tcl_GetByteArrayFromObj(pVar, &n);
sqlite3_result_blob(context, data, n, SQLITE_TRANSIENT);
- }else if( c=='b' && strcmp(zType,"boolean")==0 ){
- Tcl_GetIntFromObj(0, pVar, &n);
- sqlite3_result_int(context, n);
- }else if( c=='d' && strcmp(zType,"double")==0 ){
- double r;
- Tcl_GetDoubleFromObj(0, pVar, &r);
- sqlite3_result_double(context, r);
- }else if( (c=='w' && strcmp(zType,"wideInt")==0) ||
- (c=='i' && strcmp(zType,"int")==0) ){
- Tcl_WideInt v;
- Tcl_GetWideIntFromObj(0, pVar, &v);
+ }else if( TCL_OK == Tcl_GetWideIntFromObj(0, pVar, &v)){
sqlite3_result_int64(context, v);
+ }else if( TCL_OK == Tcl_GetDoubleFromObj(0, pVar, &r)){
+ sqlite3_result_double(context, r);
}else{
data = (unsigned char *)Tcl_GetStringFromObj(pVar, &n);
sqlite3_result_text(context, (char *)data, n, SQLITE_TRANSIENT);
@@ -1629,6 +1621,9 @@
SqlPreparedStmt *pPreStmt; /* Pointer to a prepared statement */
int rc2;

+ Tcl_ObjType *tclWideIntType = Tcl_GetObjType("wideint");
+ Tcl_ObjType *tclDoubleType = Tcl_GetObjType("double");
+
if( choice==DB_EVAL ){
if( objc<3 || objc>5 ){
Tcl_WrongNumArgs(interp, 2, objv, "SQL ?ARRAY-NAME? ?SCRIPT?");
@@ -1728,7 +1723,7 @@
assert( pPreStmt==0 );
}

- /* Bind values to parameters that begin with $ or :
+ /* Bind values to parameters that begin with $ or : or @
*/
nVar = sqlite3_bind_parameter_count(pStmt);
nParm = 0;
@@ -1744,10 +1739,10 @@
if( pVar ){
int n;
u8 *data;
- char *zType = pVar->typePtr ? pVar->typePtr->name : "";
- char c = zType[0];
+ double r;
+ Tcl_WideInt v;
if( zVar[0]=='@' ||
- (c=='b' && strcmp(zType,"bytearray")==0 && pVar->bytes==0) ){
+ ( pVar->typePtr && pVar->typePtr->name[0]=='b' && strcmp(pVar->typePtr->name,"bytearray")==0 && pVar->bytes==0) ){
/* Load a BLOB type if the Tcl variable is a bytearray and
** it has no string representation or the host
** parameter name begins with "@". */
@@ -1755,18 +1750,10 @@
sqlite3_bind_blob(pStmt, i, data, n, SQLITE_STATIC);
Tcl_IncrRefCount(pVar);
apParm[nParm++] = pVar;
- }else if( c=='b' && strcmp(zType,"boolean")==0 ){
- Tcl_GetIntFromObj(interp, pVar, &n);
- sqlite3_bind_int(pStmt, i, n);
- }else if( c=='d' && strcmp(zType,"double")==0 ){
- double r;
- Tcl_GetDoubleFromObj(interp, pVar, &r);
- sqlite3_bind_double(pStmt, i, r);
- }else if( (c=='w' && strcmp(zType,"wideInt")==0) ||
- (c=='i' && strcmp(zType,"int")==0) ){
- Tcl_WideInt v;
- Tcl_GetWideIntFromObj(interp, pVar, &v);
+ }else if( TCL_OK == Tcl_GetWideIntFromObj(interp, pVar, &v)) {
sqlite3_bind_int64(pStmt, i, v);
+ }else if( TCL_OK == Tcl_GetDoubleFromObj(interp, pVar, &r)) {
+ sqlite3_bind_double(pStmt, i, r);
}else{
data = (unsigned char *)Tcl_GetStringFromObj(pVar, &n);
sqlite3_bind_text(pStmt, i, (char *)data, n, SQLITE_STATIC);


Upd.

Допилил патч для SQLite 3.6.19. Интерфейс новой версии tclsqlite существенно переписан, и в нем исправили некоторые ошибки, причем такие, на которые не было баг-репортов. Последнее не удивительно - в старой версии ошибки интерпретатора "местами" игнорировались, а найти то, чего нет, бывает весьма сложно. В результате потратил час, вставши на эти грабли, пока понял, что проблема в неявной завязке кода патча на баг интерфейса tclsqlite. Самое смешное, что в биндинге тиклевой функции я написал код корректно, а в биндинге переменных - нет. Ниже привожу код патча для версии 3.6.19.


--- sqlite3-3.6.19.orig/src/tclsqlite.c
+++ sqlite3-3.6.19/src/tclsqlite.c
@@ -757,6 +757,8 @@
Tcl_Obj *pVar = Tcl_GetObjResult(p->interp);
int n;
u8 *data;
+ Tcl_WideInt v;
+ double r;
const char *zType = (pVar->typePtr ? pVar->typePtr->name : "");
char c = zType[0];
if( c=='b' && strcmp(zType,"bytearray")==0 && pVar->bytes==0 ){
@@ -764,18 +766,10 @@
** has no string representation. */
data = Tcl_GetByteArrayFromObj(pVar, &n);
sqlite3_result_blob(context, data, n, SQLITE_TRANSIENT);
- }else if( c=='b' && strcmp(zType,"boolean")==0 ){
- Tcl_GetIntFromObj(0, pVar, &n);
- sqlite3_result_int(context, n);
- }else if( c=='d' && strcmp(zType,"double")==0 ){
- double r;
- Tcl_GetDoubleFromObj(0, pVar, &r);
- sqlite3_result_double(context, r);
- }else if( (c=='w' && strcmp(zType,"wideInt")==0) ||
- (c=='i' && strcmp(zType,"int")==0) ){
- Tcl_WideInt v;
- Tcl_GetWideIntFromObj(0, pVar, &v);
+ }else if( TCL_OK == Tcl_GetWideIntFromObj(0, pVar, &v)){
sqlite3_result_int64(context, v);
+ }else if( TCL_OK == Tcl_GetDoubleFromObj(0, pVar, &r)){
+ sqlite3_result_double(context, r);
}else{
data = (unsigned char *)Tcl_GetStringFromObj(pVar, &n);
sqlite3_result_text(context, (char *)data, n, SQLITE_TRANSIENT);
@@ -1092,6 +1086,8 @@
if( pVar ){
int n;
u8 *data;
+ Tcl_WideInt v;
+ double r;
const char *zType = (pVar->typePtr ? pVar->typePtr->name : "");
char c = zType[0];
if( zVar[0]=='@' ||
@@ -1103,18 +1099,10 @@
sqlite3_bind_blob(pStmt, i, data, n, SQLITE_STATIC);
Tcl_IncrRefCount(pVar);
pPreStmt->apParm[iParm++] = pVar;
- }else if( c=='b' && strcmp(zType,"boolean")==0 ){
- Tcl_GetIntFromObj(interp, pVar, &n);
- sqlite3_bind_int(pStmt, i, n);
- }else if( c=='d' && strcmp(zType,"double")==0 ){
- double r;
- Tcl_GetDoubleFromObj(interp, pVar, &r);
- sqlite3_bind_double(pStmt, i, r);
- }else if( (c=='w' && strcmp(zType,"wideInt")==0) ||
- (c=='i' && strcmp(zType,"int")==0) ){
- Tcl_WideInt v;
- Tcl_GetWideIntFromObj(interp, pVar, &v);
+ }else if( TCL_OK == Tcl_GetWideIntFromObj(0, pVar, &v) ) {
sqlite3_bind_int64(pStmt, i, v);
+ }else if( TCL_OK == Tcl_GetDoubleFromObj(0, pVar, &r) ) {
+ sqlite3_bind_double(pStmt, i, r);
}else{
data = (unsigned char *)Tcl_GetStringFromObj(pVar, &n);
sqlite3_bind_text(pStmt, i, (char *)data, n, SQLITE_STATIC);

Комментариев нет:


(C) Alexey Pechnikov aka MBG, mobigroup.ru