Drivers can provide `PDO_ODBC` incorrectly converted data for long columns with stateful encodings
还没有人认领这个 Issue。
- 主要语言
- C
- 星标
- 40.4k
- 派生
- 8.1k
- 平均合并
- 2 天 13 小时
- 30 天内合并 PR
- 96
描述
Description
A user contacted me about some data that was fetched incorrectly with PDO_ODBC, but worked with PDO_IBM and procedural ODBC.
The following code:
<?php
// Db2i as the example; full dataset available on request.
$user = 'user here';
$password = 'password here';
$rdb = 'DSN here' ;
// for those unaware of the db2i options: CCSID=1208 specifies UTF-8, ALWAYSCALCLEN=1 forces the driver to provide calculate the output length of a column after character conversions, NAM=1 affects naming rules/schema order
$dsn = "DSN={{$rdb}};NAM=1;ALWAYSCALCLEN=1;CCSID=1208";
$schema = 'CSC2023' ;
$table = 'ITEM' ;
$column = 'CODE' ;
$sql = <<< NOWDOC
SELECT DESC
FROM "$schema"."$table"
WHERE $column >= ?
LIMIT 5555 OFFSET 0
NOWDOC;
$args = ["99999"];
$attrOpions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$db = new PDO("odbc:$dsn", $user, $password, $attrOpions);
$stmt = $db->prepare($sql);
$stmt->execute($args);
while (($row = $stmt->fetch())) {
var_dump($row["DESC"]);
}
$db = odbc_connect($dsn, $user, $password);
$stmt = odbc_prepare($db, $sql);
odbc_execute($stmt, $args);
while (($row = odbc_fetch_array($stmt))) {
var_dump($row["DESC"]);
}
Resulted in this output:
string(1463) "�^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^」Vァョ」W」�」T「"「^ABCDEFGHIJKLMOPR "
string(1463) "☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘ABCDEFGHIJKLMOPR "
But I expected this output instead:
string(1463) "☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘ABCDEFGHIJKLMOPR "
string(1463) "☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘☆株★■▲┐┘ABCDEFGHIJKLMOPR "
My suspicion is this is because a really annoying corner case where:
- The driver is doing character conversions
- The driver is converting from a stateful character encoding (Not like UTF-8 where it's technically stateful but you can just blithely append them and decode from the beginning to the continuation characters. I'm talking like Shift-JIS where shift in/out characters are used and you must know what state you're in before decoding a bunch of characters)
PDO_ODBCis fetching a long column, soPDO_ODBCfetches in 256 byte chunks
In this case:
- The Db2i driver does do character conversion from the EBCDIC source to the ASCII/Unicode encoding we want
- The column is in CCSID 5035 (an MBCS EBCDIC encoding that uses shift in/out characters to switch between SBCS/DBCS Japanese encodings)
- The column is over 256 bytes in length
I believe this is because, in a stateful encoding, it defaults to SBCS, and switches to DBCS with a shift characater. If I look at the hex dump of the EBCDIC string in the database, indeed, the string begins with a shift character to switch it into DBCS. There lies the problem - converting characters at the beginning is fine, but converting from the middle chunk is a problem because the shift occurs earlier in the string. Thus, it interprets DBCS data as SBCS and converts that to UTF-8 instead. (This also jacks up the length calculation.)
FWIW, I think PDO_ODBC isn't at fault here, and is really the victim - the driver failed to keep track of shift state when decoding a chunk, or it should just decode all at once and deliver substrings. (The user is reporting this issue w/ the driver to IBM, so there may be a fix for that driver at least.) However, it does show how uncommon behaviours interacting with each other can cause confusing issues - even if it's technically correct. Character encodings and lengths are annoying; dealing with keeping track of multiple is really annoying. I can imagine there could be other drivers that trigger this behaviour.
It also raises the question of why PDO_ODBC does this to begin with, which is why I bring this up. The existing behaviour might not be worth keeping if it has "fun" interactions with drivers. In #81688, me and Christoph seemed a little confused why PDO_ODBC does this to begin with, when procedural ODBC and many other languages' ODBC bindings don't. The performance benefits seems counter-intuitive (more round-trips?) and it introduces some possible corner cases (i.e. this, GH-6716, or GH-9498, which brings up 86188).
As a possible solution that might end up being similar, I wonder if it might be best to keep the bind only small data (i.e. 256 bytes; could that be raised?), and call SQLGetData with long data, but allocate and fetch the whole thing (with the size provided by the driver) all at once. It'd probably avoid confusing issues with encoding conversion/BLOBs for good. Procedural ODBC just seems to bind the larger size anyways...
FWIW, a unixODBC trace and some printfs I put down, to see what was happening: https://gist.github.com/NattyNarwhal/278aedf8e3c48bb8dcb4ae14cde0b283 - note how when the driver starts misconverting, the length stops adding up. Perhaps that could be used to abort and return an error for a bandaid fix.
PHP Version
PHP 8.3.0-DEV
Operating System
Fedora 37
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
未指定源文件或测试。首先跟踪 PDO_ODBC 的长列获取路径,尤其是 SQLGetData 的 256 字节分块,并将其与过程式 ODBC 的行为进行比较。要视为完成,需要一个可复现的有状态编码案例,以及已决定的修复方案,或保留当前行为的书面理由。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- c, php
- 领域
- databases
- Issue 类型
- 缺陷
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 停滞
- 描述清晰度
- 需要澄清
- 新手友好度
- 25/100