Skip to content

Oracle 9i (fv2): fix describe desync on NOT-NULL columns, report null_ok#218

Merged
lemenkov merged 1 commit into
masterfrom
oracle-9i-fv2-describe-nullok-97
Jul 3, 2026
Merged

Oracle 9i (fv2): fix describe desync on NOT-NULL columns, report null_ok#218
lemenkov merged 1 commit into
masterfrom
oracle-9i-fv2-describe-nullok-97

Conversation

@lemenkov

@lemenkov lemenkov commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What

Fixes the 9i field-version-2 describe decoder (decode_fv2_describe), which desynced on any NOT NULL column.

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):

  • nullable0x01, read as a width-1 ub4 whose value coincidentally equals the name length → looked correct.
  • NOT NULL0x00, read as width-0/value-0 (one byte) → stream slips, name garbles (USERNAMEb'\x08USERNAM'), and a multi-column NOT-NULL fetch dies with ORA-03115.

Every offline fixture was SELECT <literal> AS name FROM dual — literals are always nullable, so null_ok was always 0x01 and the slip never triggered. Real dictionary/table columns are NOT NULL and reproduce it.

Changes

  • oracle/tns.py — read null_ok + the 1-byte byte-length explicitly, then the genuine ub4 char-length. Shared decoder, so sync and async are covered.
  • Bonus: null_ok now feeds Cursor.description[6]; 9i column nullability was previously hardcoded nullable.
  • tests/test_tns_decode.py — regression fixture from a live 9.2.0.4 capture of two identical-length names differing only in NOT NULL vs nullable (the exact blind spot); asserts names + null_ok.
  • docs/PROTOCOL.md §19.1 — corrected the layout; documented the null_ok trap; dropped the "validated against eight live captures" over-claim (all eight were nullable dual-aliases).

Testing

  • Live 9.2.0.4: all_users.username, all_tables (2-col), and a NOT-NULL/nullable table — names correct, ORA-03115 gone, null_ok accurate.
  • Offline suite green (80 decode / 527 full).
  • 10g + 23ai describe (DCB path) unaffected.

Root-caused via a controlled NOT-NULL-vs-nullable experiment and cross-checked against an independent 9i client implementation.

Closes #217

The per-column tail of the fv2 describe (0x62 RPA) is
`<OAC> null_ok(1B) namelen_bytes(1B) ub4(namelen_chars) DALC(name) 00 00`.
`decode_fv2_describe` read the first two bytes as a ub4 name-length, which
only works when the leading byte is 0x01. That byte is actually `null_ok`
(0x00 NOT NULL, 0x01 nullable): a nullable column sends 0x01, read as a
width-1 ub4 whose value coincidentally equals the name length, so the bug
stayed hidden. A NOT-NULL column sends 0x00, which decode_ub4 reads as
width-0/value-0 (one byte) — the column stream slips, the name garbles
(USERNAME -> b'\x08USERNAM'), and a multi-column NOT-NULL fetch then dies
with ORA-03115.

Every offline fixture was `SELECT <literal> AS name FROM dual`; a literal is
always nullable, so null_ok was always 0x01 and the slip never triggered.

Read null_ok + the 1-byte byte-length explicitly, then the genuine ub4
char-length. null_ok now feeds Cursor.description[6], so 9i column
nullability is reported instead of being hardcoded nullable. Fix lives in
the shared tns.py decoder, so sync and async both get it.

Added an offline regression fixture from a live 9.2.0.4 capture of two
identical-length names differing only in NOT NULL vs nullable, and updated
PROTOCOL.md §19.1. Verified live on 9.2.0.4 (all_users, all_tables, a
NOT-NULL/nullable table) and confirmed the 10g+ DCB path is unaffected.

Closes #217
refs #97

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
@lemenkov lemenkov added this to the 1.10.2 milestone Jul 3, 2026
@lemenkov
lemenkov merged commit 4dd0326 into master Jul 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oracle 9i (fv2): describe desyncs on NOT-NULL columns (null_ok byte misread as ub4 name-length)

1 participant