Java reference client (tests/reference/RefClient.java) drives the
official ifxjdbc.jar through three controlled scenarios:
- connect-only: bare connect+disconnect
- select-1: SELECT 1 round-trip with column metadata
- dml-cycle: CREATE TEMP + INSERT + SELECT in one connection
All three work end-to-end against the dev container with the
documented credentials (informix/in4mix on sysmaster).
Wire traffic captured via socat MITM relay (no sudo needed) — listen
on 9090, forward to 9088, hex-dump both directions. Captures saved
to docs/CAPTURES/. Total ~24 KB across the three scenarios.
PROTOCOL_NOTES.md cross-reference findings (§12):
Confirmed against the wire (✅ both JDBC + PCAP):
- Big-endian framing throughout
- Login PDU structure matches encodeAscBinary field-by-field
- Server response matches DecodeAscBinary
- Post-login messages are bare [short tag][payload]
- SQ_EOT (=12) is a per-PDU flush/submit marker, not just
disconnect ack — every logical request ends with [short 0x000c]
Wire findings that AMENDED the JDBC-derived hypothesis:
- The "capability section" is actually three 4-byte negotiated
capability ints (Cap_1, Cap_2, Cap_3), not one int + 8 reserved
zero bytes. The CFR decompile read it as adjacent zero writes
but the wire shows distinct values that the server echoes back.
Trust the wire over the decompiler for byte layouts.
Validated post-login execution:
- The first SELECT after login is JDBC-internal (locale lookup
via informix.systables) — a Python implementation doesn't need
to do this housekeeping
- SQ_PREPARE format observed: [short SQ_PREPARE=2][short flags=0]
[int sqlLen][bytes sql][nul][short ?][short ?][short SQ_EOT=12]
- Server sends [short SQ_DESCRIBE=8] followed by column metadata
Phase 0 exit verdict: GO. All four hard exit criteria confirmed.
Remaining gaps (result-set descriptor exact layout, statement-time
errors, capability semantics) are bounded and tractable in Phase 2.
The narrow-scope off-ramp is not needed.
586 lines
26 KiB
Markdown
586 lines
26 KiB
Markdown
# 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:
|
||
```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 |