从一个bug谈谈psqlodbc游标的一点认识
本文源于最近修正的一个关于psqlodbc的bug,该bug在近期的psqlodbc的git上也有人提交了修正。
关于该bug的修正代码可以看这里:
https://git.postgresql.org/gitweb/?p=psqlodbc.git;a=commit;h=85f6fade3
说道这个bug,我们要提ODBC提供的两个API函数:SQLBulkOperations 和 SQLSetPos。
关于这两个函数,它们的用处是:
SQLBulkOperations执行大容量插入和大容量书签操作(包括update、 delete和fetch)。
SQLSetPos函数设置在行集中的游标位置,并允许应用程序刷新行集中的数据或用于更新或删除在结果集中的数据。当利用SQLSetPos删除数据时,操作设置为 SQL_DELETE并且将RowNumber设置为要删除的行号(当设置RowNumber为0时删除结果集中的所有数据)
说白了就是你先用SQLExecute或者SQLExecDirect函数执行SQL文获取一个结果集,然后对这个结果集进行操作。bug发生的场景是DELETE的时候,即我们对结果集中的数据进行删除时发生。在对结果集的数据进行删除时,会调用Adddeleted()函数将结果集中被删除的数据的行号、状态信息和oid,ctid等做记录,保存在一个结构体数组中。而结构体数组在设计是要求按照递增的顺序进行存储的,所以你进行每一次删除可能就要对这个结构体数组做排序操作。问题就在这个排序过程中,排序的时候本应该是依次遍历数组,每次for循环结束count+1,结果它这里直接是count+num_field(这个是结果集的列数),所以,只要返回的结果集的列数大于1,bug就应该发生。
似乎就是这样清楚明白。
我们按照以下的构造再现程序:
(1) 调用SQLSetStmtAttr函数设置SQL文的SQL_ATTR_ROW_ARRAY_SIZE属性值大于1;
(2) 调用SQLExecute或者SQLExecDirect函数执行SQL文;
(3) 调用SQLFetch或SQLFetchScroll函数获取结果集;
(4) (3)中返回的结果集的列数大于1;
(5) (3)中返回的结果集的行数大于1;
(6) 调用ODBC的API函数SQLSetPos或者SQLBulkOperations删除(2)中返回的结果集中的数据;
(7) (6)中删除的记录行数超过一行
说明:SQL_ATTR_ROW_ARRAY_SIZE
指定每次调用SQLFetch或SQLFetchScroll函数返回的结果集行数。 它也用于指定SQLBulkOperations函数的大容量书签操作中的书签数组中的行数。 默认值为 1。
让程序返回了50条数据,但是我们就是再现不出来。无奈,无意中我把返回的结果集增大到了200,结果居然在现了。
奇怪,这个bug和结果集的行数无关啊。
带着这个疑问,我调试了代码:
在AddDeleted()函数的开头,有这样的语句:
if (!QR_get_cursor(res))
return TRUE;
当结果集函数较小时,直接在这里就返回了。。。也就是说这个时候没有游标了。可是使用SQLExecute()执行SQL、文是默认有游标打开的,程序从游标中获取数据的。不服输的我又在测试程序开头显示地指定了游标:
SQLSetCursorName(hstmt, "C1", SQL_NTS);
还是一模一样的结果。很奇怪。于是我打开了ODBC数据源的mylog和commonlog:
发现:
这个游标居然被关闭了!!!
不可思议。
于是我查询了相关资料,得到以下的认识:
3.ODBC数据源的cache size
每次执行SQLExecute或者SQLExecDirect函数执行SQL文时,程序底层是调用游标一次性返回cache size大小行数的结果集到ODBC数据源的缓存。如果SQL文返回的结果集行数不超过cache size(即ODBC数据源的缓存能缓存下SQL文的所有结果集),那么ODBC数据源会关闭该游标。否则ODBC数据源会保持该游标。
也就是说,这个是ODBC自己的缓存机制。
于是我修改再现程序:
(1) 调用SQLSetStmtAttr函数设置SQL文的SQL_ATTR_ROW_ARRAY_SIZE属性值大于1;
(2) 调用SQLExecute或者SQLExecDirect函数执行SQL文;
(3) (2)中SQL文返回的行数大于ODBC数据源的cache size;
(4) 调用SQLFetch或SQLFetchScroll函数获取结果集;
(5) (4)中返回的结果集的列数大于1;
(6) (4)中返回的结果集的行数大于1;
(7) 调用ODBC的API函数SQLSetPos或者SQLBulkOperations删除(2)中返回的结果集中的数据;
(8) (7)中删除的记录行数超过一行
再现了该bug。