Oninit® 4gl2everywhere — SQLI Front-End
4gl2everywhere terminates the Informix client/server (SQLI) wire protocol on the front end. Each TCP connection is a token stream — a sequence of <u16 BE token id><token-specific payload> framings concluded by SQ_EOT — that the bridge reads, walks token-by-token, and dispatches to a per-token handler. Per-token handlers emit the matching SQLI responses: SQ_DONE for completed operations, SQ_ERR for errors, and typed PFPDUs (the LOGIN accept ASF body) for connection setup. Result-set tokens (SQ_TUPLE) are produced on the SQL execution path.
Token catalogue
The bridge handles the subset of SQLI tokens stock c4gl- compiled binaries actually emit. Connection-association tokens (SQ_LOGIN_ASC, SQ_PROTOCOLS, SQ_ACCEPT), SQL-execution tokens (SQ_PREPARE, SQ_EXECUTE, SQ_BIND, SQ_OPEN, SQ_NFETCH, SQ_CLOSE, SQ_RELEASE), transaction control (SQ_BEGIN, SQ_CMMTWORK, SQ_RBWORK), database control (SQ_DBOPEN, SQ_DBCLOSE, SQ_DISCONNECT), and the response side (SQ_DONE, SQ_ERR, SQ_TUPLE). Numeric token values are the on-the-wire contract.
Connection lifecycle
A 4GL session over the bridge follows a fixed token sequence:
| Step | Tokens | What happens |
|---|---|---|
| TCP connect | (TCP) | Client opens a socket to the bridge's listener port. |
| Login | SQ_LOGIN_ASC, CONACC ASF body | Client sends SQ_LOGIN_ASC plus an ASF body; bridge replies with the CONACC ASF body. |
| Protocol negotiation | SQ_PROTOCOLS | Client and bridge exchange capability bitmaps so each side knows the other's supported wire-format variants. |
| Database open | SQ_DBOPEN, SQ_DONE | Client sends SQ_DBOPEN with the database name; bridge translates to the configured target backend's database-switch verb and replies SQ_DONE. |
| SQL traffic | SQ_PREPARE / SQ_BIND / SQ_OPEN / SQ_NFETCH / SQ_CLOSE, or single-shot SQ_COMMAND; each burst ends in SQ_EOT | Statement lifecycle (cursor-based) or single-shot DML. Each request ends in SQ_EOT; each response ends in SQ_EOT. |
| Transaction control | SQ_BEGIN / SQ_CMMTWORK / SQ_RBWORK | Delimit transactional groups. |
| Database close | SQ_DBCLOSE | Client sends SQ_DBCLOSE; bridge releases the database-scoped state on the backend. |
| Disconnect | SQ_DISCONNECT, SQ_DONE | Client sends SQ_DISCONNECT; bridge replies SQ_DONE, runs the cleanup checklist, closes the TCP socket. |
Each step is independently observable in the per-session SQL trace.
Login handshake
A connection opens with the client sending SQ_LOGIN_ASC (token magic 0x7371, ASCII "sq") followed by an ASF (Application Support Facility) PFPDU carrying cmd / user / password / version / mode / default_db / flags plus KEY=VALUE environment variables. The bridge decodes the inbound LOGIN, logs the non-credential fields (never the password), and replies with a CONACC (sltype = 2) ASF response.
The CONACC body completes the session-layer handshake; from this point on the client treats the connection as an established Informix session and proceeds to protocol negotiation.
Character set and locale
The SQ_LOGIN_ASC ASF body carries the client's locale and charset in its KEY=VALUE environment block — typically CLIENT_LOCALE, DB_LOCALE, GL_USEGLU, GL_DATE, DBDATE. The bridge logs the negotiated locale for operator visibility and forwards it to the backend's locale-aware connection setup (PostgreSQL client_encoding, MySQL SET NAMES, Oracle NLS_LANG).
The most common source of mismatch is a c4gl client shipping CLIENT_LOCALE=en_US.819 (Informix's CP1252 alias) against a UTF-8 backend; the bridge passes the value through and lets the backend either negotiate down to a compatible encoding or reject the connection. Locale rejections surface as SQ_ERR with category BACKEND.
Protocol negotiation
After the LOGIN accept, both sides exchange SQ_PROTOCOLS tokens carrying a capability bitmap. The bitmap tells each side which wire-format variants the peer supports. Two flags matter for what the bridge emits and accepts on the wire:
| Capability flag | Effect when negotiated |
|---|---|
| u32 SQL-text length | SQ_COMMAND and SQ_PREPARE carry a u32 SQL-text length instead of the legacy u16. Informix 9.0342+ clients negotiate this. |
| IUS tuple format | SQ_TUPLE carries a u32 row-data length at offset +4 instead of the legacy u16 at offset +6. Informix 9.0334+ servers always negotiate this, and modern IDS clients always advertise it. |
The bridge speaks both legacy and modern variants — the two sides negotiate down to the common subset, and the per-session token walker uses the negotiated bits when sizing tokens that have multiple wire layouts.
Wire-protocol version range
The bridge speaks the SQLI variants Informix has shipped across the 9.x and later product lines. Two version-driven inflection points matter on the wire:
| Server version | Wire-layout change |
|---|---|
| 9.0334+ | IUS-mode tuple layout. SQ_TUPLE moved its row-data length from a u16 at offset +6 to a u32 at offset +4, allowing rows past 64 KiB and surfacing the per-row warning count as a separate field. Modern IDS servers always negotiate this. |
| 9.0342+ | Widened SQL-text length. SQ_COMMAND and SQ_PREPARE moved from a u16 sql-text length to a u32, allowing single statements past 64 KiB. |
Both flags are negotiated through the SQ_PROTOCOLS capability bitmap. The bridge falls back to the legacy u16 layouts when an older client (Informix 7.x / 8.x c4gl binary) does not advertise the modern bits, and uses the modern layouts for any client that does.
Database control
SQ_DBOPEN carries u16 dbname-length + dbname (even-padded) + u16 mode trailer. The bridge validates the dbname against the Informix-legal identifier grammar (letter or underscore, then letters / digits / underscore / $ / ., length 1..128) and dispatches to the configured backend, which translates the verb to its native database-switch primitive — see Backends for the per-target table.
The mode trailer (Informix exclusive / quiescent modes) is parsed but not forwarded; those modes don't translate cleanly to non-Informix targets, and discarding them is the right behaviour for the bridge's role as a translation layer.
SQ_DBCLOSE is the bare token. The bridge calls the backend's database-release primitive (a no-op on most backends; on PostgreSQL it resets the schema search-path).
Statement lifecycle
A 4GL SELECT typically traces the following sequence:
| Step | Tokens | What happens |
|---|---|---|
| 1 | SQ_PREPARE | Client sends statement text + bind-parameter count. Bridge parses, lowers to a dialect-independent IR, rewrites per target dialect, and returns a statement id via SQ_DONE. |
| 2 | SQ_CURNAME + SQ_BIND | Client names a cursor and supplies bind values; each bind carries type, indicator, precision, and value bytes. |
| 3 | SQ_OPEN | Client opens the cursor; the bridge submits the rewritten SQL to the backend and prepares to stream results. |
| 4 | SQ_NFETCH loop | Client requests N rows. Bridge returns one SQ_TUPLE per row, terminated with SQ_DONE when the result set is exhausted. |
| 5 | SQ_CLOSE | Client closes the cursor. |
| 6 | SQ_RELEASE | Client frees the prepared-statement resources. |
Single-shot DML (UPDATE, INSERT, DELETE) takes the simpler SQ_COMMAND path: SQL text in, SQ_DONE (or SQ_ERR) out, no cursor lifecycle.
Bind parameter encoding
SQ_BIND carries the runtime parameter values for a prepared statement. Wire shape:
u16 SQ_BIND u16 parameter count foreach parameter: u16 type code (SQL type from the type-class catalogue) u16 indicator (0 = value present, 0xFFFF = SQL NULL) u16 precision word (high byte = digits, low byte = scale) bytes value (type-driven)
Per-type value layouts mirror the standard Informix wire encoding:
| SQL type | Wire encoding |
|---|---|
| SQLSMINT / SQLINT / SQLINT8 / SQLBIGINT | 2 / 4 / 8 byte big-endian signed. |
| SQLDATE | 4 bytes BE; Informix days-since-1899-12-31. |
| SQLSMFLOAT / SQLFLOAT | 4 / 8 byte IEEE-754, big-endian byte order. |
| SQLCHAR / SQLVCHAR / SQLNCHAR / SQLNVCHAR | u16 length + length bytes (even-padded). |
| SQLDECIMAL / SQLMONEY | u16 length + control byte + base-100 digit pairs; precision word encodes (digits<<8) | scale. |
| SQLDTIME (DATETIME) / SQLINTRV (INTERVAL) | u16 length + qualifier byte + BCD digits; precision word encodes (total_digits<<8) | ((start_q<<4) | end_q). |
| SQLBYTES / SQLTEXT | 56-byte locator placeholder; the BLOB content streams separately as SQ_BBIND followed by one or more SQ_BLOB tokens (terminated by an empty SQ_BLOB(0)). |
The bridge validates each binding against the prepared statement's parameter signature before forwarding to the backend. Type aliasing observed on the wire — c4gl emits NCHAR / NVARCHAR host-vars as plain SQLCHAR / SQLVCHAR, and lets the server's catalog drive any further conversion — is preserved transparently.
Column metadata: DESCRIBE
SQ_DESCRIBE returns the column layout for a prepared statement, telling the client what types and lengths to expect on each row of the result set. The bridge produces a DESCRIBE response whose column list mirrors the underlying backend's reflection (the target driver's column-info call); each column entry carries type code, declared length, NULL-ability flag, and column name.
c4gl issues SQ_DESCRIBE after PREPARE and uses the response to allocate fetch buffers ahead of the NFETCH loop. SQ_NDESCRIBE is the no-payload variant: client sends just the token plus a statement id and the bridge replies with the same column layout. The DESCRIBE step is where the bridge maps the backend's column types back to Informix type codes — the fetch loop then sees the column layout the 4GL runtime expects, regardless of which backend produced the rows.
Result row encoding
Each result row is one SQ_TUPLE token. Wire shape:
u16 SQ_TUPLE (= 0x000E)
u16 warning_count (server warning count for this row)
u32 row_data_len (with the IUS capability flag negotiated;
legacy u16 at offset +6 otherwise)
bytes per-column values, concatenated, in the order DESCRIBE reported
The TUPLE itself does not re-state column types — the client already knows the layout from the preceding DESCRIBE response and walks the row payload accordingly. Per-column value layout follows the same per-type table the BIND token uses: fixed-size big-endian integers for SMINT / INT / INT8 / DATE / SMFLOAT / FLOAT, length-prefixed for VARCHAR / VCHAR, qualifier-then-BCD for DATETIME / INTERVAL, locators for BYTE / TEXT.
NULL is signalled by reserved sentinel values per type (Informix convention): 0x8001 for SMINT NULL, 0x80000001 for INT NULL, 0x8000000000000001 for INT8 / BIGINT NULL, 0x80000001 for DATE NULL. NULL on string columns is indicated by an out-of-band marker the per-column getter consults.
When the result set is exhausted the bridge emits SQ_DONE; on a backend-side error mid-stream the bridge emits SQ_ERR and closes the cursor.
Transaction control
Three transaction-boundary tokens delimit transactional groups:
| Token | Effect |
|---|---|
| SQ_BEGIN / SQ_BEGIN_NOREPL | Start a transaction. The NOREPL variant suppresses replication for this transaction (Informix- only semantic; on non-Informix targets it collapses to a regular begin). |
| SQ_CMMTWORK | Commit the active transaction. |
| SQ_RBWORK | Roll back the active transaction; carries a u16 rollback-mode trailer for the savepoint variant. |
The bridge forwards each verb to the backend's transaction primitive (begin, commit, rollback on the backend interface). The bridge does not insert implicit commits or rollbacks — transactional semantics are preserved as the 4GL program expressed them.
Savepoints
Three savepoint-related tokens extend the transaction model:
| Token | Effect |
|---|---|
| SQ_SQLISETSVPT | Establish a named savepoint inside the active transaction. Carries u16 name-length + savepoint-name (even-padded) + u16 mode trailer. |
| SQ_SQLIRELSVPT | Release a named savepoint. |
| SQ_SQLIRBACKSVPT | Roll back to a named savepoint without ending the transaction. |
The bridge translates each verb to the target backend's standard SQL savepoint primitives (SAVEPOINT name, RELEASE SAVEPOINT name, ROLLBACK TO SAVEPOINT name); every supported backend has SQL-standard savepoint support, so the mapping is direct.
Disconnection
A clean disconnect arrives as SQ_DISCONNECT. The bridge replies SQ_DONE, then runs the per-session cleanup checklist: roll back any open transaction, disconnect the backend, close the SQL trace file, close the client socket, decrement the listener's session count. Cleanup runs exactly once per session — see Sessions & Threading.
A TCP close from the client without an explicit SQ_DISCONNECT (process kill, network drop, idle-timeout reaper) follows the same cleanup path. Half-closed peers do not crash the listener: every send uses MSG_NOSIGNAL, and SIGPIPE is ignored process-wide.
Connection rejection
Not every connection completes the handshake. The bridge rejects connections in three failure modes, each surfaced as an SQ_ERR with SQLCODE -746 and a specific IFXBRIDGE category before the TCP socket is closed:
| Category | Trigger | Body string |
|---|---|---|
| CONNECTION_LIMIT | The listener has reached its configured max_sessions cap. | "Maximum sessions reached" |
| SHUTDOWN | The listener is in graceful drain (post-SIGTERM / SIGINT) and is no longer accepting new sessions. | "Listener draining" |
| BACKEND_DOWN | The listener could not connect to the configured target backend at session start. | "Backend unavailable" |
4GL programs match on the canonical category to distinguish capacity rejection from backend outage from ongoing drain — three operationally distinct failure modes that an Informix-only client otherwise would not have a way to disambiguate.
Token framing
Token sizes are computed token-by-token from the wire-format size rules. Categories:
| Category | Tokens | Wire layout |
|---|---|---|
| 2-byte token-only | SQ_BEGIN, SQ_EOT, SQ_DBCLOSE, SQ_OPEN, SQ_EXECUTE, SQ_RELEASE, SQ_DISCONNECT | The token id is the entire wire span; no payload. |
| 4-byte token + u16 | SQ_ID, SQ_RBWORK | Two bytes of token id followed by two bytes of payload. |
| Length-prefixed string | SQ_CURNAME, SQ_PROTOCOLS | Token id + u16 length + bytes. |
| Length-prefixed string + trailer | SQ_DBOPEN, SQ_SQLISETSVPT | Token id + u16 length + bytes + u16 mode trailer. |
| SQL-text-bearing | SQ_COMMAND / SQ_PREPARE | Token id + u16 bind-count + sql-text length + sql_text. The sql-text length is u16 on legacy clients and u32 on 9.0342+ clients (negotiated via the capability flag above). |
String fields are even-padded so the next token starts on an even offset.
Multi-token requests
A single client request commonly carries multiple tokens in one TCP burst, terminated by a single SQ_EOT. The bridge's token walker iterates each token in order and dispatches to its handler; the response is also a token sequence terminated by SQ_EOT. A typical OPEN-cursor burst:
SQ_ID + statement-id SQ_CURNAME + cursor-name SQ_BIND + bind list SQ_OPEN SQ_EOT
Multi-token batching keeps the network round-trip count low: a single PREPARE + OPEN + first-NFETCH exchange is four or five tokens in each direction, not four or five TCP round trips. The bridge processes each token in the same order the client sent it, so transactional sequencing is preserved.
Errors back to the client
Errors travel as SQ_ERR tokens carrying i16 sqlcode + i16 isam_code + u16 message-length + message bytes (even-padded), followed by SQ_EOT. Message bodies use the canonical IFXBRIDGE:CATEGORY:DETAIL format — see Errors for the category list and reserved SQLCODEs.
Per-session protocol observability
Each session carries its own SQL trace file (when trace is enabled) that records every token the bridge processed, with a one-line summary per request: token name + byte count. The trace is deliberately line-oriented so tail / grep / awk work without preprocessing — see SQL Trace for the file-naming convention and per-listener overrides.
Operational events (listener bind, session start / cleanup, drain, reload) are recorded separately in the bridge's main log; the trace captures the SQLI traffic, the log captures the listener lifecycle.