# 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 `#()` in the decompiled tree (see `JDBC_NOTES.md`) - 🔵 **PCAP**: observed in `docs/CAPTURES/.pcap` at offset `` - ✅ **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: ```java 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[])`: ```java 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: ```java 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. Cross-check matrix | Phase 0 milestone | 🟡 JDBC | 🔵 PCAP | Status | |-------------------|--------|--------|--------| | Wire framing primitives (endianness, widths, padding) | ✅ | ⬜ | 🟠 unverified | | Login byte layout (binary path) | ✅ | ⬜ | 🟠 unverified | | Disconnection | ✅ | ⬜ | 🟠 unverified | | Message-type tag enumeration | ✅ | n/a | ✅ source-of-truth | | `SELECT 1` request layout | partial (hypothesis) | ⬜ | 🟠 needs PCAP | | Result-set framing | partial (hypothesis) | ⬜ | 🟠 needs PCAP | | Error response (login) | ✅ | ⬜ | 🟠 unverified | | Error response (statement-time) | ⬜ | ⬜ | 🟠 needs both | **Phase 0 exit gate** requires the first three rows AND the `SELECT 1` row promoted to ✅. That's the explicit work remaining: capture login + `SELECT 1` + disconnect, validate the JDBC-derived hypotheses against the bytes, fill in the gaps for the result-set framing.