Implements end-to-end round-trip for BYTE (type 11) and TEXT (type 12)
columns. Python bytes/bytearray map to BYTE; str is auto-encoded as
ISO-8859-1 for TEXT.
Wire protocol — write side:
* SQ_BIND payload carries a 56-byte blob descriptor with size at offset
[16..19] (per IfxBlob.toIfx). NULL is byte 39=1.
* After all per-param blocks, SQ_BBIND (41) declares blob count, then
chunked SQ_BLOB (39) messages stream the actual bytes (max 1024
bytes/chunk per JDBC), terminated by zero-length SQ_BLOB.
* Then SQ_EXECUTE proceeds normally.
Wire protocol — read side:
* SQ_TUPLE returns only the 56-byte descriptor; actual bytes live in
the blobspace.
* For each BYTE/TEXT column in each row, send SQ_FETCHBLOB with the
descriptor and read SQ_BLOB chunks until zero-length terminator.
* The locator is only valid while the cursor is open — must dereference
BEFORE sending CLOSE. Doing it after returns -602 (Cannot open blob).
Server-side prerequisites (one-time setup):
1. blobspace: onspaces -c -b blobspace1 -p /path -o 0 -s 50000
2. logged DB: CREATE DATABASE testdb WITH LOG
3. config + archive:
onmode -wm LTAPEDEV=/dev/null
onmode -wm TAPEDEV=/dev/null
onmode -l
ontape -s -L 0 -t /dev/null
Without #3, JDBC fails identically to our driver with "BLOB pages can't
be allocated from a chunk until chunk add is logged". This identical
failure was the diagnostic confirmation that our protocol bytes were
correct — same server response = byte-for-byte parity.
Tests: 9 integration tests in tests/test_blob.py — single-chunk,
multi-chunk (5120 bytes), NULL, multi-row, binary-safe, TEXT roundtrip,
ISO-8859-1, NULL TEXT, mixed columns. Plus the Phase 4
test_unsupported_param_type_raises was updated since bytes is no longer
the canonical unsupported type — switched to a custom class.
Total: 53 unit + 107 integration = 160 tests.
The smart-LOB family (BLOB/CLOB) is a separate state-machine extension
deferred to Phase 9 — it uses IfxLocator + LO_OPEN/LO_READ session
protocol against sbspace, not the BBIND/BLOB stream.
197 lines
8.1 KiB
Java
197 lines
8.1 KiB
Java
// Phase 0 spike: reference client for capturing the Informix SQLI wire protocol.
|
|
//
|
|
// Drives the official IBM Informix JDBC driver (com.informix.jdbc.IfxDriver)
|
|
// through controlled scenarios so that tcpdump captures of localhost:9088
|
|
// can be cross-referenced against the decompiled JDBC source. This is the
|
|
// "ground truth" client whose traffic IS by definition spec-correct.
|
|
//
|
|
// Build:
|
|
// javac -cp build/ifxjdbc.jar tests/reference/RefClient.java -d build/
|
|
// Run:
|
|
// java -cp build/ifxjdbc.jar:build/ tests.reference.RefClient <scenario>
|
|
//
|
|
// Scenarios: connect-only, select-1, dml-cycle, all
|
|
//
|
|
// Connection params can be overridden via env vars; defaults are the
|
|
// Informix Developer Edition Docker image defaults documented in
|
|
// docs/DECISION_LOG.md.
|
|
|
|
package tests.reference;
|
|
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.ResultSetMetaData;
|
|
import java.sql.Statement;
|
|
import java.sql.SQLException;
|
|
|
|
public class RefClient {
|
|
|
|
static final String HOST = env("IFX_HOST", "127.0.0.1");
|
|
static final String PORT = env("IFX_PORT", "9088");
|
|
static final String SERVER = env("IFX_SERVER", "informix");
|
|
static final String DATABASE = env("IFX_DATABASE", "sysmaster");
|
|
static final String USER = env("IFX_USER", "informix");
|
|
static final String PASSWORD = env("IFX_PASSWORD", "in4mix");
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Class.forName("com.informix.jdbc.IfxDriver");
|
|
|
|
String scenario = args.length > 0 ? args[0] : "connect-only";
|
|
switch (scenario) {
|
|
case "connect-only": runConnectOnly(); break;
|
|
case "select-1": runSelect1(); break;
|
|
case "dml-cycle": runDmlCycle(); break;
|
|
case "byte-cycle": runByteCycle(); break;
|
|
case "all":
|
|
runConnectOnly();
|
|
runSelect1();
|
|
runDmlCycle();
|
|
break;
|
|
default:
|
|
System.err.println("Unknown scenario: " + scenario);
|
|
System.err.println("Valid: connect-only | select-1 | dml-cycle | all");
|
|
System.exit(2);
|
|
}
|
|
}
|
|
|
|
static String url() {
|
|
return String.format(
|
|
"jdbc:informix-sqli://%s:%s/%s:INFORMIXSERVER=%s",
|
|
HOST, PORT, DATABASE, SERVER
|
|
);
|
|
}
|
|
|
|
static void log(String fmt, Object... args) {
|
|
System.out.printf("[ref] " + fmt + "%n", args);
|
|
}
|
|
|
|
static String env(String key, String dflt) {
|
|
String v = System.getenv(key);
|
|
return (v == null || v.isEmpty()) ? dflt : v;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Scenario A: connect + immediate disconnect
|
|
// -------------------------------------------------------------------
|
|
static void runConnectOnly() throws SQLException {
|
|
log("=== connect-only ===");
|
|
log("URL: %s", url());
|
|
try (Connection c = DriverManager.getConnection(url(), USER, PASSWORD)) {
|
|
log("connected; product=%s", c.getMetaData().getDatabaseProductName());
|
|
// Note: do NOT call getDatabaseProductVersion() — the 4.50.JC10
|
|
// JDBC driver throws NumberFormatException on Informix 15.0's
|
|
// version string ("150."). Connection itself works fine.
|
|
}
|
|
log("disconnected.");
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Scenario B: connect + SELECT 1 + disconnect
|
|
// -------------------------------------------------------------------
|
|
static void runSelect1() throws SQLException {
|
|
log("=== select-1 ===");
|
|
try (Connection c = DriverManager.getConnection(url(), USER, PASSWORD);
|
|
Statement s = c.createStatement();
|
|
ResultSet rs = s.executeQuery("SELECT 1 FROM systables WHERE tabid = 1")) {
|
|
|
|
ResultSetMetaData md = rs.getMetaData();
|
|
log("columns: %d", md.getColumnCount());
|
|
for (int i = 1; i <= md.getColumnCount(); i++) {
|
|
log(" [%d] name=%s type=%d (%s) precision=%d scale=%d",
|
|
i, md.getColumnName(i), md.getColumnType(i),
|
|
md.getColumnTypeName(i), md.getPrecision(i), md.getScale(i));
|
|
}
|
|
int rowNum = 0;
|
|
while (rs.next()) {
|
|
rowNum++;
|
|
log(" row %d: col1=%s", rowNum, rs.getObject(1));
|
|
}
|
|
log("rows returned: %d", rowNum);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Scenario C: full DML cycle (CREATE / INSERT / SELECT / DROP)
|
|
// Uses a UUID-suffixed table to be safe to re-run.
|
|
// -------------------------------------------------------------------
|
|
static void runDmlCycle() throws SQLException {
|
|
log("=== dml-cycle ===");
|
|
String table = "spike_" + Long.toHexString(System.nanoTime());
|
|
try (Connection c = DriverManager.getConnection(url(), USER, PASSWORD)) {
|
|
c.setAutoCommit(true); // sysmaster is unlogged; force autocommit
|
|
|
|
try (Statement s = c.createStatement()) {
|
|
log("CREATE TEMP TABLE %s", table);
|
|
s.execute("CREATE TEMP TABLE " + table +
|
|
" (id INTEGER, name VARCHAR(50), val FLOAT)");
|
|
}
|
|
|
|
try (PreparedStatement ps = c.prepareStatement(
|
|
"INSERT INTO " + table + " VALUES (?, ?, ?)")) {
|
|
ps.setInt(1, 42);
|
|
ps.setString(2, "hello");
|
|
ps.setDouble(3, 3.14);
|
|
int n = ps.executeUpdate();
|
|
log("INSERT rowcount=%d", n);
|
|
}
|
|
|
|
try (Statement s = c.createStatement();
|
|
ResultSet rs = s.executeQuery("SELECT id, name, val FROM " + table)) {
|
|
ResultSetMetaData md = rs.getMetaData();
|
|
log("SELECT columns:");
|
|
for (int i = 1; i <= md.getColumnCount(); i++) {
|
|
log(" [%d] %s : %s", i, md.getColumnName(i), md.getColumnTypeName(i));
|
|
}
|
|
while (rs.next()) {
|
|
log(" row: id=%d name=%s val=%f",
|
|
rs.getInt(1), rs.getString(2), rs.getDouble(3));
|
|
}
|
|
}
|
|
// Temp table dropped automatically on disconnect; no DROP needed.
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Scenario D: BYTE column write+read cycle. Requires:
|
|
// - logged DB (env IFX_DATABASE=testdb)
|
|
// - a blobspace named "blobspace1" already created
|
|
// Set IFX_DATABASE=testdb before running.
|
|
// -------------------------------------------------------------------
|
|
static void runByteCycle() throws SQLException {
|
|
log("=== byte-cycle ===");
|
|
String table = "byte_" + Long.toHexString(System.nanoTime());
|
|
try (Connection c = DriverManager.getConnection(url(), USER, PASSWORD)) {
|
|
c.setAutoCommit(true);
|
|
|
|
try (Statement s = c.createStatement()) {
|
|
log("CREATE TABLE %s (id INT, data BYTE IN blobspace1)", table);
|
|
s.execute("CREATE TABLE " + table + " (id INT, data BYTE IN blobspace1)");
|
|
}
|
|
|
|
byte[] payload = "hello bytes from JDBC".getBytes();
|
|
try (PreparedStatement ps = c.prepareStatement(
|
|
"INSERT INTO " + table + " VALUES (?, ?)")) {
|
|
ps.setInt(1, 1);
|
|
ps.setBytes(2, payload);
|
|
int n = ps.executeUpdate();
|
|
log("INSERT rowcount=%d (sent %d bytes)", n, payload.length);
|
|
}
|
|
|
|
try (Statement s = c.createStatement();
|
|
ResultSet rs = s.executeQuery("SELECT id, data FROM " + table)) {
|
|
while (rs.next()) {
|
|
byte[] got = rs.getBytes(2);
|
|
log(" row: id=%d data.len=%d data=%s", rs.getInt(1),
|
|
got.length, new String(got));
|
|
}
|
|
}
|
|
|
|
try (Statement s = c.createStatement()) {
|
|
s.execute("DROP TABLE " + table);
|
|
}
|
|
}
|
|
}
|
|
}
|