Summary
decode_fv2_describe (the 9i field-version-2 describe-in-RPA path, PROTOCOL.md §19.1) misparses the per-column metadata for any NOT NULL column, garbling the column name and desyncing the stream.
Root cause
The per-column tail after the OAC is:
<OAC-fv2> null_ok(1B) namelen_bytes(1B) ub4(namelen_chars) DALC(name) 00 00
The decoder read the first two bytes as a ub4 name-length. The leading byte is actually null_ok (0x00 = NOT NULL, 0x01 = nullable):
- A nullable column sends
0x01, which decode_ub4 reads as a width-1 integer whose value coincidentally equals the name length — so it looked correct.
- A NOT NULL column sends
0x00, read as width-0/value-0 (consuming one byte). The stream slips: the name garbles (USERNAME → b'\x08USERNAM'), and a multi-column NOT-NULL fetch fails with ORA-03115 (garbled describe → bad define block).
Why it was missed
Every offline fixture (and the original RE captures) was SELECT <literal> AS name FROM dual. A literal/expression is always nullable, so null_ok was always 0x01 and the slip never triggered. Real dictionary/table columns (all_users.username, all_tables.table_name) are NOT NULL and reproduce it immediately.
Reproduction (live 9.2.0.4)
SELECT username FROM all_users -- name decodes to '\x08USERNAM'
SELECT table_name, x FROM all_tables -- ORA-03115 on fetch
Fix
Read null_ok + the 1-byte byte-length explicitly, then the genuine ub4 char-length. Also feeds null_ok into Cursor.description[6] — 9i column nullability was previously hardcoded to nullable.
Confirmed via a controlled experiment (two identical-length names differing only in NOT NULL vs nullable) and collaboration/cross-check with an independent 9i client implementation.
Summary
decode_fv2_describe(the 9i field-version-2 describe-in-RPA path, PROTOCOL.md §19.1) misparses the per-column metadata for any NOT NULL column, garbling the column name and desyncing the stream.Root cause
The per-column tail after the OAC is:
The decoder read the first two bytes as a
ub4name-length. The leading byte is actuallynull_ok(0x00= NOT NULL,0x01= nullable):0x01, whichdecode_ub4reads as a width-1 integer whose value coincidentally equals the name length — so it looked correct.0x00, read as width-0/value-0 (consuming one byte). The stream slips: the name garbles (USERNAME→b'\x08USERNAM'), and a multi-column NOT-NULL fetch fails with ORA-03115 (garbled describe → bad define block).Why it was missed
Every offline fixture (and the original RE captures) was
SELECT <literal> AS name FROM dual. A literal/expression is always nullable, sonull_okwas always0x01and the slip never triggered. Real dictionary/table columns (all_users.username,all_tables.table_name) are NOT NULL and reproduce it immediately.Reproduction (live 9.2.0.4)
Fix
Read
null_ok+ the 1-byte byte-length explicitly, then the genuineub4char-length. Also feedsnull_okintoCursor.description[6]— 9i column nullability was previously hardcoded to nullable.Confirmed via a controlled experiment (two identical-length names differing only in NOT NULL vs nullable) and collaboration/cross-check with an independent 9i client implementation.