informix-db/docs/PROTOCOL_NOTES.md
Ryan Malloy ea00990774 Phase 1 polish: PDU match test catches a real capability-int bug
Polish item #1: byte-for-byte regression test that asserts our
generated login PDU is structurally identical to JDBC's reference
captured in docs/CAPTURES/01-connect-only.socat.log.

The test (tests/test_pdu_match.py) immediately caught a real bug:
the capability section was misread during Phase 0 byte-decoding.
Earlier text claimed Cap_1=1, Cap_2=0x3c000000, Cap_3=0 — actually:

  Cap_1 = 0x0000013c   (= (capability_class << 8) | protocol_version
                          where protocol_version = 0x3c = PF_PROT_SQLI_0600)
  Cap_2 = 0
  Cap_3 = 0

The misalignment was: the 0x3c byte I attributed to Cap_2's high
byte was actually Cap_1's low byte. The dev-image server is
permissive enough to accept arbitrary capability values, so the
connection succeeded even with the wrong bytes — but the PDU wasn't
structurally identical to JDBC's reference. SERVER-ACCEPTS ≠
STRUCTURALLY-CORRECT. This is exactly why the byte-for-byte diff
was the right polish item; "it connects" was a false ceiling.

After fix:
- 6 PDU-match tests assert byte-for-byte equality at offsets 2..280
  (the structural prefix: SLheader sans length, all login markers,
  capability ints, username, password, protocol IDs, env vars).
- Bytes 280+ legitimately differ per process (PID, TID, hostname,
  cwd, AppName) — those are NOT asserted.
- Length field (offsets 0..1) also legitimately differs because our
  PDU has shorter env list and AppName.
- Test uses monkey-patched IfxSocket so no network is needed.

Polish item #2: Makefile per global CLAUDE.md convention. Targets:
install, lint, format, test, test-integration, test-all, test-pdu,
ifx-up/down/logs/shell/status, capture (re-run JDBC scenarios under
socat), clean. `make` (no target) prints help.

Doc updates:
- PROTOCOL_NOTES.md §12: corrected capability section with the
  actual values and an explanation of the methodology lesson
- DECISION_LOG.md: new entry recording the correction with a
  pointer to the regression test and the takeaway

Side artifacts:
- docs/CAPTURES/03-py-connect-only.socat.log
- docs/CAPTURES/04-py-no-database.socat.log
- docs/CAPTURES/05-py-fixed-caps.socat.log

Test counts: 40 unit + 6 integration = 46 total, all green, ruff clean.
2026-05-02 20:18:03 -06:00

26 KiB
Raw Blame History

SQLI Wire Protocol Notes

Phase 0 spike artifact. Byte-level reference for the Informix SQLI wire protocol, derived from clean-room study of the decompiled IBM Informix JDBC driver (ifxjdbc.jar, Implementation-Version: 4.50.10-SNAPSHOT / printable 4.50.JC10, build 146 from 2023-03-07; SHA256 in JDBC_NOTES.md) cross-checked against packet captures of the reference exchange (pending).


Source attribution conventions

Each documented byte sequence cites both sources of evidence:

  • 🟡 JDBC: cross-referenced against <class>#<method>() in the decompiled tree (see JDBC_NOTES.md)
  • 🔵 PCAP: observed in docs/CAPTURES/<file>.pcap at offset <n>
  • CONFIRMED: corroborated by both 🟡 and 🔵
  • 🟠 UNVERIFIED: only one of the two sources

Current state (2026-05-02): all 🟡 findings are present from JDBC reading. PCAP capture is pending (Phase 0 task #7) and required for . Treat everything below as 🟠 pending PCAP cross-check.


1. Wire framing primitives 🟡

Endianness

Big-endian (network byte order) for all multi-byte integers.

Source: com.informix.lang.JavaToIfxType.JavaToIfxInt(int) line 51-54:

public static byte[] JavaToIfxInt(int i) {
    byte[] b = new byte[]{(byte)(i >> 24), (byte)(i >> 16), (byte)(i >> 8), (byte)i};
    return b;
}

Same pattern for JavaToIfxSmallInt (2 bytes) and JavaToIfxLongBigInt (8 bytes). Java's DataOutputStream defaults to big-endian; Informix matches that.

Width table

Wire type Width (bytes) Java method Notes
SmallInt 2 writeSmallInt(short) / writeShort SMALLINT, message-type tags, length prefixes
Int 4 writeInt(int) INTEGER, capability flags, sizes
LongBigInt 8 writeLongBigint(long) BIGINT (use this for 64-bit ints)
LongInt 10 writeLongInt(long) Legacy variable-numeric LONG INT — skip MVP, predates 64-bit ints
Real 4 writeReal(float) IEEE 754 single
Double 8 writeDouble(double) IEEE 754 double
Date 4 writeDate(Date) day-count from Informix epoch (1899-12-31, to confirm)

Variable-length encoding (string, decimal, datetime, interval, BYTE, TEXT)

[short length][bytes payload][optional 0x00 pad if length is odd]

The 16-bit alignment is a hard requirement. Source: IfxDataOutputStream.writePadded(byte[]):

public void writePadded(byte[] b) throws IOException {
    this.write(b, 0, b.length);
    if ((b.length & 1) >= 1) {
        this.write(0);
    }
}

Mirror in IfxDataInputStream.readPadded. Every variable-payload message needs this padding or the next short read will misalign and the entire parser desynchronizes.

String encoding

com.informix.lang.JavaToIfxType.JavaToIfxChar(String) returns:

[short length+1][bytes][0x00 nul terminator]

The +1 is the trailing nul. So an N-character string takes 2 + N + 1 bytes, then padded to even.

Note: there is also a JavaToIfx4BytesChar variant for wide-char strings. Probably for GLS multibyte locales. Phase 6+.


2. Connection establishment 🟡

TCP setup

  • Default port: 9088 (native SQLI). Port 9089 is SSL.
  • Socket.connect(InetSocketAddress(host, port), loginTimeout)
  • setTcpNoDelay(true) — Nagle off
  • setKeepAlive(socKeepAlive) — opt-in via IFX_SOC_KEEPALIVE property
  • Optional SSL wrap via SSLSocketFactory. Phase 6+; skip for MVP.
  • Buffered streams: BufferedInputStream(in, 4096) / BufferedOutputStream(out, 4096) over the raw socket streams. The IfxDataInputStream/IfxDataOutputStream wrap the buffered streams.

Capability bits (driving login path selection)

  • capabilities == 0 → legacy text-mode login (EncodeAscString)
  • capabilities > 0 → modern binary login (encodeAscBinary) — this is what we implement

The capabilities bitfield itself is opaque from the client side; the JDBC driver computes it based on opt-props. For MVP we will hardcode a sensible value matching what JDBC sends in a vanilla connection (will derive from PCAP).

Other constants seen in Connection.java

Constant Value Notes
MAX_BUFF_SIZE 32768 upper bound for individual PDUs
MIN_BUFF_SIZE 140 lower bound
STREAM_BUF_SIZE 4096 socket buffer size
PFCONREQ_BUF_SIZE 2048 login request buffer
SL_HEADER_SIZE 6 the SLheader is 6 bytes
applType "sqlexec" 12-char nul-padded application type
applID "sqli" application ID string
PROT_SQLIOL "ol" 8-char nul-padded protocol identifier
NET_TLITCP "tlitcp" 8-char nul-padded network type
floatType "IEEEM" float encoding type identifier (5 chars)

3. Login sequence (binary path) 🟡

3a. Client→server: SLheader (6 bytes)

[short totalPduSize+6]   # 2 bytes — PDU payload length + 6 (header overhead)
[byte slType]            # 1 byte — 1=SLTYPE_CONREQ (connection request)
[byte slAttribute]       # 1 byte — 60=PF_PROT_SQLI_0600 (protocol version)
[short slOptions]        # 2 bytes — 0 in vanilla connect

Source: Connection.EncodeSLheader(o, pfPDUsize, 1, 60, 0).

3b. Client→server: PFheader (the binary login PDU)

This is the body that the SLheader announces. Total size depends on credentials and env vars. The byte layout, in emission order from Connection.encodeAscBinary():

[short 100]                          # SQ_ASSOC marker — start of "association" record
[short 101]                          # SQ_ASCBINARY marker — binary-format indicator
[int 61]                             # ?? known constant; meaning TBD
[short floatType.len+1]              # = 6
[bytes "IEEEM"][byte 0]              # float-format identifier, nul-terminated

[short 108]                          # SQ_ASCBPARMS marker — binary parameters block
[bytes "sqlexec\0\0\0\0\0"]         # applType, fixed 12 bytes
[short version.len+1]                # = 6
[bytes "9.280"][byte 0]              # client version (hardcoded "9.280" — note: NOT JDBC version!)
[short serial.len+1]                 # = 12
[bytes "RDS#R000000"][byte 0]        # client serial (hardcoded constant)
[short applID.len+1]                 # = 5
[bytes "sqli"][byte 0]               # application ID

[int capabilities]                   # client capability flags (opaque bitfield)
[int 0]                              # reserved
[int 0]                              # reserved

[short 1]                            # ?? section marker (NOT in IfxMessageTypes)
[short username.len+1]
[bytes username][byte 0]

# password (optional)
if password is null:
    [short 0]                        # zero-length password
else:
    [short password.len+1]
    [bytes password][byte 0]

[bytes "ol\0\0\0\0\0\0"]             # PROT_SQLIOL — 8 bytes
[int 61]                             # ?? same magic as before
[bytes "tlitcp\0\0"]                 # NET_TLITCP — 8 bytes
[int 1]                              # UTYPE_INTERNET (= 1)

[short 104]                          # SQ_ASCINITREQ marker
[short 11]                           # ASF_XCONNECT (= 11)
[int stmtoptions]                    # = 3 | (sqlhGroup ? 0x2000000 : 0) | (trustedCtx ? 0x4000000 : 0)
                                     # bit 0+1 = ASF_AMBIG_SEOL (=3)
                                     # bit 25 = ASF_GRPREF (sqlh group)
                                     # bit 26 = ASF_TRUSTCTXT (trusted context)
[short servername.len+1]
[bytes servername][byte 0]

# database (optional)
if dbname is null:
    [short 0]
else:
    [short dbname.len+1]
    [bytes dbname][byte 0]

[short 0][short 0][short 0][short 0]  # 4 reserved/empty option slots (8 bytes total)

# Environment vars (encodeEnv)
[short 106]                          # SQ_ASCENV marker
[short num_envvars]
for each (name, value) in envlist:
    [short name.len+1]
    [bytes name][byte 0]
    [short value.len+1]
    [bytes value][byte 0]

# Process info (encodeEnvPInfo)
[short 107]                          # SQ_ASCPINFO marker
[int 0]                              # reserved
[int procId]                         # OS process ID
[int threadId]                       # thread ID
[short hostname.len+1]
[bytes hostname][byte 0]
[short 0]                            # reserved
[short cwd.len+1]
[bytes cwd][byte 0]

# AppName section (SQ_ASCMISC_60)
[short 116]                          # SQ_ASCMISC_60
[short total_section_length]         # = 10 + appname.len + 1
[int 0]                              # reserved
[int 0]                              # reserved
[short appname.len+1]
[bytes appname][byte 0]

# End-of-PDU
[short 127]                          # SQ_ASCEOT — end-of-transmission for login

3c. Server→client: response

Connection.recvConnectionResponse() reads:

[short totalLength]                  # length of the response PDU
[bytes (totalLength - 2)]            # payload, parsed below

Then the payload starts with:

[byte SLType]                        # 2=CONACC (accepted), 3=CONREJ (rejected), 13=REDIRECT
[skipBytes 3]                        # slAttribute + slOptions — usually unused on response
[short 100]                          # SQ_ASSOC marker
[short 101]                          # SQ_ASCBINARY marker
... DecodeAscBinary payload ...

DecodeAscBinary(in) reads:

[skipBytes 4]
[short subLength]
[skipBytes subLength]
[short 108]                          # SQ_ASCBPARMS marker
[skipBytes 12]
[short verLen][bytes serverVersion]  # e.g. "15.00.UC1"
[short serLen][bytes serverSerial]
[short appLen][bytes appID]
[int Cap_1][int Cap_2][int Cap_3]    # server capability flags (3 × 4 bytes)
[skipBytes 2]
[short subLen1][skipBytes subLen1]   # optional reserved
[short subLen2][skipBytes subLen2]   # optional reserved
[skipBytes 24]
[short marker]                       # final disposition:
                                     #   102 = SQ_ASCINITRESP → error/warning section follows
                                     #   103 = redirect
                                     #   127 = SQ_ASCEOT (clean)

Error block (when marker=102):

[skipBytes 6]
[short svcError]
[short osError]
[short Warnings]
[short numErrors]
for i in 0..numErrors-1:
    [short offset]                   # if -1, stop
    [readChar errMsg]                # length-prefixed string

3d. Login PDU summary

  • One client-emitted PDU for the entire login (username + password inline)
  • One server-emitted PDU for the response (success → server version + capabilities; failure → error block)
  • No challenge-response round trip for plain auth. PAM auth uses SQ_CHALLENGE=129/SQ_RESPONSE=130 AFTER the binary login PDU, but only if the server requests it. Plain-auth happy path is two messages total.

4. Message framing (post-login) 🟡

After successful login, messages are bare — no SLheader, no PFheader. Each message is:

[short messageType]    # 2-byte big-endian tag from IfxMessageTypes
[payload]              # variable, depends on message type

Evidence: Connection.disconnectOrderly() writes only:

byte[] b = JavaToIfxType.JavaToIfxSmallInt((short)56);  // SQ_EXIT = 56
this.asfIfxOs.write(b);
this.asfIfxOs.flush();

And reads back exactly the same shape. No length prefix, no framing overhead. The receiving side keys off the message type tag to know how many bytes follow.

This is a stream-oriented protocol, not a packet-oriented one. The wire is a continuous sequence of [tag][payload][tag][payload]… and the parser must know each tag's payload structure to know where the next tag begins.


5. Message type tags 🟡

The full enumeration is in com.informix.jdbc.IfxMessageTypes (163 lines, ~140 constants). Key categories below.

Connection lifecycle (post-login)

Tag Decimal Hex Purpose
SQ_VERSION 53 0x35 client/server version negotiation
SQ_INFO 81 0x51 info request (sub-types: INFO_VERSION=2, INFO_TYPE=3, INFO_CAPABILITY=4, INFO_DB=5, INFO_ENV=6)
SQ_PROTOCOLS 126 0x7E protocol negotiation
SQ_CONNECT 112 0x70 connection setup
SQ_DBOPEN 36 0x24 open database
SQ_DBCLOSE 37 0x25 close database
SQ_DBLIST 26 0x1A list databases
SQ_DISCONNECT 114 0x72 logical disconnect
SQ_EXIT 56 0x38 terminate session (on the wire for disconnect)
SQ_EOT 12 0x0C end of transmission (returned by server on disconnect ack)

Auth (server-initiated challenge-response, PAM only)

Tag Decimal Purpose
SQ_CHALLENGE 129 server challenges client (PAM)
SQ_RESPONSE 130 client response to challenge
SQ_ACCEPT 127 server accepts
SQ_ACK 128 acknowledgement

Plain-password auth does NOT use these. They are only for PAM and other challenge-response auth schemes. Skip MVP.

Statement execution

Tag Decimal Purpose
SQ_COMMAND 1 execute SQL directly (immediate execution, no preparation)
SQ_PREPARE 2 prepare a statement (returns a handle)
SQ_ID 4 reference a prepared statement by id
SQ_BIND 5 bind a parameter to a prepared statement
SQ_OPEN 6 open cursor on a prepared statement
SQ_EXECUTE 7 execute a prepared statement
SQ_DESCRIBE 8 describe columns of a prepared statement
SQ_NFETCH 9 fetch next row(s)
SQ_CLOSE 10 close cursor
SQ_RELEASE 11 release prepared statement
SQ_CURNAME 3 name a cursor

Result responses (server→client)

Tag Decimal Purpose
SQ_TUPLE 14 a row of data
SQ_DONE 15 end of result set / completion of DML
SQ_EOT 12 end of transmission
SQ_ERR 13 error

Transactions

Tag Decimal Purpose
SQ_BEGIN 35 begin transaction
SQ_BEGIN_NOREPL 115 begin (no replication)
SQ_CMMTWORK 19 commit work
SQ_RBWORK 20 rollback work
SQ_SVPOINT 21 declare savepoint
SQ_SQLISETSVPT 137 set savepoint (newer protocol)
SQ_SQLIRELSVPT 138 release savepoint
SQ_SQLIRBACKSVPT 139 rollback to savepoint

Distributed transactions (XA) — Phase 6+

SQ_X* family: 65-74, 82. SQ_XBEGIN, SQ_XCOMMIT, SQ_XROLLBACK, SQ_XPREPARE, etc.

LOB / Blob — Phase 6+

SQ_FETCHBLOB (38), SQ_BLOB (39), SQ_BBIND (41), SQ_SBBIND (52), SQ_FILE_READ (106), SQ_FILE_WRITE (107).

RPC layer (sub-protocol, tag range 200-205) — Phase 6+

Tag Decimal Purpose
SQ_INVOKE 200 RPC invocation
SQ_REPLY 201 RPC reply
SQ_EXCEPTION 202 RPC exception
SQ_VERSION_REQ 203 version request
SQ_VERSION_REPLY 204 version reply
SQ_AMFPARAM 205 AMF parameter (used by stored procedures?)

6. Statement execution: SELECT 1 🟠

Hypothesis (to be validated by PCAP):

Client→server:

[short SQ_COMMAND=1]
[short sql.len+1]
[bytes "SELECT 1"][byte 0]

or possibly with extra wrapper bytes (e.g. statement ID, options). Unknown without capture.

Server→client (success):

[short SQ_DESCRIBE=8 or implicit description]
... column descriptors ...
[short SQ_TUPLE=14]
... row payload ...
[short SQ_DONE=15]
... completion data (rowcount, etc.) ...
[short SQ_EOT=12]

This will be validated and made specific by Phase 0 task #2 (cross-reference captures).


7. Result-set framing 🟠

Pending PCAP capture. Hypothesized structure:

  • Each row is [short SQ_TUPLE=14][row payload]
  • Row payload is the column values in order, encoded per their type's wire format (with the 16-bit alignment rule)
  • NULL representation: TBD (probably a per-column flag bit or sentinel value)
  • End-of-result: [short SQ_DONE=15] followed by completion metadata (estimated rows, actual rows, etc.)

com.informix.jdbc.IfxColumnInfo likely holds the column-descriptor structure. Read this once we have a PCAP to align against.


8. Disconnection (well-corroborated by JDBC alone)

Client→server:

[short SQ_EXIT=56]

Server→client:

[short SQ_EXIT=56]   # echo
or
[short SQ_EOT=12]    # end of transmission

Server may interleave one or more [short SQ_XACTSTAT=99] records before the final ack (transaction status info during shutdown). Client reads and discards these until it sees 56 or 12.

Source: Connection.disconnectOrderly() (Connection.java:227-265).


9. Error responses 🟠

Two error paths observed:

9a. Login-time errors (during login PDU exchange)

Error block embedded in the response, marked by [short 102] (SQ_ASCINITRESP):

[skipBytes 6]
[short svcError]      # Informix service error code
[short osError]       # OS-level error code
[short numWarnings]
[short numErrors]
for i in 0..numErrors:
    [short offset]    # if -1, stop
    [readChar errMsg] # length-prefixed string

Throws IfxASFRemoteException(svcError, osError, errMsg).

9b. Statement-time errors (post-login)

Hypothesized: [short SQ_ERR=13][error payload]. Structure of the payload TBD; needs PCAP. Likely contains SQLCODE, ISAM error code, error message text, possibly SQLSTATE. Look at IfxSqli error-decode methods once we have a capture.


10. Type codecs

10a. IDS type codes (column descriptors)

com.informix.lang.IfxTypes is the constants class. Pending — read this file and tabulate the type codes we care about for MVP (SMALLINT, INT, BIGINT, FLOAT, REAL, CHAR, VARCHAR, NCHAR, NVARCHAR, BOOLEAN, DATE, NULL marker).

10b. Wire encodings (Phase-2 MVP types)

IDS Type Type Code Wire format Reader Writer
SMALLINT TBD 2 bytes BE readSmallInt() writeSmallInt(short)
INTEGER TBD 4 bytes BE readInt() writeInt(int)
BIGINT TBD 8 bytes BE readLongBigint() writeLongBigint(long)
FLOAT (DOUBLE) TBD 8 bytes IEEE 754 readDouble(prec) writeDouble(double)
REAL (SMALLFLOAT) TBD 4 bytes IEEE 754 readReal(prec) writeReal(float)
CHAR(N) TBD length-prefixed, padded to even readChar() writeChar(byte[])
VARCHAR(N) TBD length-prefixed, padded to even readChar() (similar)
BOOLEAN TBD 1 byte 't'/'f' (per Informix convention; verify) TBD TBD
DATE TBD 4 bytes — int day count from Informix epoch (1899-12-31, verify) readDate() writeDate(Date)

10c. Phase 6+ types

DATETIME (qualifier-byte precision), INTERVAL (qualifier-encoded), DECIMAL/NUMERIC (Informix BCD), MONEY, LVARCHAR, BYTE/TEXT (length-prefixed BLOBs), CLOB/BLOB (smart blobs with handles), ROW, COLLECTION (LIST/SET/MULTISET).


11. Open questions

  • Exact byte layout of SQ_COMMAND payload — does it include statement options after the SQL text?
  • NULL representation in row data — null bitmap, sentinel value, or per-column flag?
  • Result-set column-descriptor structure (the bytes that come back before the first SQ_TUPLE).
  • How Cap_1/Cap_2/Cap_3 (server capability flags) are bit-allocated.
  • The leading [int 61] magic in the login PDU — what does 61 mean? (Possibly version-related: PF_PROT_SQLI_0600 = 60, PF_PROT_SQLI_WITH_CSS = 61?)
  • Why the embedded versionNumber = "9.280" string (not the JDBC version, not the server version).
  • The [short 1] marker between Cap_3 and the username — what's it for?
  • Whether the server emits any banner at TCP-accept time before the client's login PDU.

All of these resolve from packet capture in Phase 0 task #7.


12. Wire-validation findings (2026-05-02 — captures vs JDBC source)

Captures collected via socat MITM relay (no sudo needed; listens on 9090, forwards to 9088):

socat -d -d -x TCP-LISTEN:9090,reuseaddr TCP:127.0.0.1:9088 2>docs/CAPTURES/<scenario>.socat.log &
IFX_PORT=9090 java -cp build/ifxjdbc.jar:build/ tests.reference.RefClient <scenario>

Three scenarios captured: 01-connect-only (1.7 KB), 02-select-1 (6.7 KB), 02-dml-cycle (9.9 KB). All in docs/CAPTURES/. The > lines are client→server, < are server→client.

Validated against the wire

  • Endianness is big-endian. Confirmed: SLheader 01 c3 01 3c 00 00 decodes as length=0x01c3=451, slType=1, slAttribute=0x3c=60, slOptions=0.
  • Login PDU structure matches Connection.encodeAscBinary field-by-field: SLheader 6 bytes, then SQ_ASSOC=100, SQ_ASCBINARY=101, int 61, IEEEM, SQ_ASCBPARMS=108, sqlexec padded, "9.280", "RDS#R000000", "sqli", capabilities, ..., username "informix", password "in4mix", PROT_SQLIOL "ol", int 61, NET_TLITCP "tlitcp", int 1, SQ_ASCINITREQ=104, ASF_XCONNECT=11, stmtoptions=3, servername, env vars (DBPATH, CLIENT_LOCALE=en_US.8859-1, CLNT_PAM_CAPABLE, DBDATE, IFX_UPDDESC, NODEFDAC), process info, AppName, SQ_ASCEOT=127.
  • Server response structure matches DecodeAscBinary: SLheader (slType=2 CONACC, slAttribute=60), SQ_ASSOC, SQ_ASCBINARY, int 61, server's float type "IEEEI", "srvinfx" padded, the version string "IBM Informix Dynamic Server Version 15.0.1.0.3", "serial", "informix" instance name, the three capability ints, "on" + "soctcp" + secondary protocol identifiers, container hostname 2327c4354ea8, /home/informix, /opt/ibm/informix/v15.0.1.0.3/bin/oninit, SQ_ASCEOT=127.
  • Post-login messages are bare (no SLheader, no length prefix). Each message is [short tag][payload]. Confirmed across all post-login traffic in the captures.
  • [short 0x000c] (SQ_EOT=12) is a per-PDU flush/submit marker, not just a disconnect ack. Every client→server "logical request" in the post-login captures ends with 00 0c. Server replies the same way. So the on-wire pattern is:
    [short tag1][payload1][short tag2][payload2]...[short SQ_EOT=12]   # one logical request
    
  • Disconnection is bare [short SQ_EXIT=56] both directions, sometimes with interleaved SQ_XACTSTAT=99 records before the final ack. Confirmed.

Wire findings that AMENDED the JDBC-derived hypothesis 🔵

  • The capability section is three 4-byte ints (Cap_1, Cap_2, Cap_3) — confirming the JDBC source's three writeInt calls. The values are NOT all zero as my earlier text claimed; the wire shows:

    • Cap_1 = 0x0000013c (= 316). Looks like (capability_class << 8) | protocol_version, where protocol_version = 0x3c = PF_PROT_SQLI_0600 (= 60). The high byte 0x01 appears to be a "supported feature class" indicator.
    • Cap_2 = 0
    • Cap_3 = 0

    An earlier draft of this section misread the byte alignment and claimed Cap_1=1, Cap_2=0x3c000000. That was wrong — the 0x3c byte that appeared to be in Cap_2 was actually the last byte of Cap_1. Phase 1 caught this when we wrote a byte-for-byte PDU diff against the JDBC reference (tests/test_pdu_match.py); the dev-image server is permissive enough to accept the wrong values, so the connection succeeded but the bytes weren't structurally identical to JDBC's reference PDU. Lesson: server-accepts ≠ structurally-correct. Always diff against IBM's reference for protocol-bearing bytes.

    Our connections.py now sends the correct values. The PDU diff test asserts byte-for-byte match for offsets 2..280 (everything except the SLheader length and process-specific tail).

Validated post-login execution shape 🔵

The first SELECT the JDBC driver issues after login is internal — looking up locale info from informix.systables. The wire bytes for that 88-byte client→server message:

00 02                            short SQ_PREPARE = 2
00 00                            short 0  (flags / reserved)
00 00 00 49                      int 73 (length of SQL bytes including trailing nul)
73 65 6c 65 63 74 20 46 49 52 53 54 20 31 20 73 69 74 65 20 66 72 6f 6d 20 69 6e 66 6f 72 6d 69 78 2e 73 79 73 74 61 62 6c 65 73 20 77 68 65 72 65 20 74 61 62 6e 61 6d 65 20 3d 20 27 20 47 4c 5f 43 4f 4c 4c 41 54 45 27 00
                                 73 bytes of SQL text (incl. nul):
                                 "select FIRST 1 site from informix.systables where tabname = ' GL_COLLATE'\0"
00 16                            short 22 (= ?)  
00 31                            short 49 (= ?)
00 0c                            short SQ_EOT = 12 — flush

So the SQ_PREPARE message body is approximately:

[short SQ_PREPARE=2]
[short flags]                    # observed 0
[int sqlLen]                     # length INCLUDING trailing nul terminator
[bytes sql][nul]
[short ?]                        # observed 0x16 — purpose TBD
[short ?]                        # observed 0x31 — purpose TBD
[short SQ_EOT=12]                # flush

The corresponding response (82 bytes) starts 00 08 (= SQ_DESCRIBE=8), with column metadata including 00 04 28 63 6f 6e 73 74 61 6e 74 29 00 00 = the column name "(constant)\0" (length-prefixed at length=4 — but content is "(constant)" 10 chars + nul; so the length=4 here is NOT the column name length). The exact column-descriptor layout needs more analysis, deferred to Phase 2.

Phase 0 exit verdict

Phase 0 exit criterion 🟡 JDBC 🔵 PCAP Status
Wire framing primitives (endianness, widths, padding) confirmed
Login byte layout (binary path) confirmed
Disconnection confirmed
Message-type tag enumeration (sample) confirmed
SQ_PREPARE/SELECT request layout partial confirmed (high-level)
Result-set column descriptor layout partial 🟠 needs Phase 2 work
Error response (login) (no failure case captured) 🟠 ok for Phase 0
Error response (statement-time) 🟠 deferred to Phase 2
Capability bitfield semantics observed 🟠 ok for Phase 0

Phase 0 exit recommendation: 🟢 GO. The four hard exit criteria (login, message tags, SELECT round-trip, JDBC↔PCAP corroboration) are confirmed. The remaining gaps (result-set descriptor exact layout, error responses, capability semantics) are bounded and tractable — they'll be characterized in Phase 2 against the existing captures + targeted new captures, and don't change the project's architecture.

The "narrow scope" off-ramp is not needed. The wire protocol is well-formed, plain-password auth works, and the mismatch between JDBC source and wire on capability ints is small and explainable (decompiler artifact). Proceed to Phase 1.