diff --git a/docs/PROTOCOL_NOTES.md b/docs/PROTOCOL_NOTES.md index db91896..2638617 100644 Binary files a/docs/PROTOCOL_NOTES.md and b/docs/PROTOCOL_NOTES.md differ diff --git a/tests/reference/RefClient.java b/tests/reference/RefClient.java new file mode 100644 index 0000000..9114b07 --- /dev/null +++ b/tests/reference/RefClient.java @@ -0,0 +1,154 @@ +// 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 +// +// 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 "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. + } + } +}