// 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 "byte-cycle": runByteCycle(); break; case "blob-cycle": runBlobCycle(); 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); } } } // ------------------------------------------------------------------- // Scenario E: BLOB (smart-LOB) write+read cycle. Requires: // - logged DB (env IFX_DATABASE=testdb) // - sbspace1 (smart-large-object space) already created // Set IFX_DATABASE=testdb before running. // ------------------------------------------------------------------- static void runBlobCycle() throws SQLException { log("=== blob-cycle ==="); String table = "blob_" + 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 BLOB)", table); s.execute("CREATE TABLE " + table + " (id INT, data BLOB)"); } byte[] payload = "hello smart-LOB blob 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); } } } }