SELECT on BLOB or CLOB columns no longer requires raw byte interpretation. The 72-byte server-side locator is wrapped in a typed BlobLocator or ClobLocator (frozen dataclass) so the column is recognizable as "server-side reference, not actual bytes". Wire-protocol findings: * Smart-LOB columns DON'T appear with their nominal type codes (102/101) in SQ_DESCRIBE. They surface as UDTFIXED (41) with extended_id 10 (BLOB) or 11 (CLOB) and encoded_length=72 (locator size). * Retrieving the actual bytes requires SQ_FPROUTINE (103) RPC to invoke ifx_lo_open, plus SQ_LODATA (97) for chunked transfer, plus another SQ_FPROUTINE for ifx_lo_close. That's a Phase 10 lift — roughly 2x the protocol surface of Phase 8. Server config needed (added to Phase 7 setup): * sbspace: onspaces -c -S sbspace1 ... * default sbspace: onmode -wm SBSPACENAME=sbspace1 What ships in Phase 9: * informix_db.BlobLocator(raw: bytes) — 72-byte frozen wrapper * informix_db.ClobLocator(raw: bytes) — distinct type, same shape * Row decoder branch in _resultset.parse_tuple_payload * Wire constants SQ_LODATA=97, SQ_FPROUTINE=103, SQ_FPARAM=104 Tests: * 11 unit tests in test_blob_locator_unit.py (no Informix needed) — construction, immutability, equality, hash, repr safety, size validation. * 4 integration tests in test_smart_lob.py — fixture seeds via JDBC reference client (smart-LOB writes also need deferred protocols). * RefBlob.java helper in tests/reference/ for seeding via JDBC. Total: 64 unit + 111 integration = 175 tests. Locator design note: __repr__ omits the raw bytes (they're opaque to the client). Same-bytes locators of different families compare unequal — BlobLocator(x) != ClobLocator(x) — to avoid silent type confusion.
239 lines
9.9 KiB
Java
239 lines
9.9 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 "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);
|
|
}
|
|
}
|
|
}
|
|
}
|