From df3bf9c4c5f3585245935df0e228522dbf72bacb Mon Sep 17 00:00:00 2001 From: slayerjain Date: Mon, 17 Aug 2026 14:31:37 +0530 Subject: [PATCH] feat(mysql-dual-conn): add a FLOAT/DOUBLE/BIGINT UNSIGNED fidelity fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keploy/keploy#4426: keploy read MySQL FLOAT and DOUBLE result-set columns with a numeric conversion instead of an IEEE-754 reinterpret, so a column holding 9.99 was recorded as 1.0926057e+09 (FLOAT) and 4.621813488089437e+18 (DOUBLE). The corruption happened at record time, so it survived re-recording and replay asserted against a value the database never returned. Nothing in the e2e suite round-tripped a real typed row through record and replay, which is why the defect went unnoticed. This adds the fixture that would have caught it. - init.sql: a numeric_fidelity table carrying FLOAT, DOUBLE and BIGINT UNSIGNED columns, seeded with 9.99 plus sign, zero, whole and subnormal cases, and with BIGINT UNSIGNED values above MaxInt64 (no lossless float64 form, so a mock format routing them through one collapses distinct rows onto the same number). - GET /api/oms/numerics selects those columns and GET /api/oms/float-param/{v} binds a FLOAT parameter, covering the result-set decode and the COM_STMT_EXECUTE parameter decode respectively. Both endpoints bind a parameter deliberately. Without one, JdbcTemplate issues a plain Statement, Connector/J sends COM_QUERY, and MySQL answers with a text result set whose values are all length-encoded strings — which never touches the binary decode this fixture exists to cover. The OMS datasource already runs with useServerPrepStmts=true, so a bound parameter is all it takes to get a binary-protocol result set. Verified locally against keploy at 1b6ead43 (buggy) and the #4426 fix: the fixture records 1.0926057e+09 / 4.621813488089437e+18 on the former and 9.99 / 9.99 on the latter, and replay goes 4/6 -> 6/6. Signed-off-by: slayerjain --- mysql-dual-conn/init.sql | 30 +++++++++++++ .../mysqlreplicate/QueryController.java | 45 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/mysql-dual-conn/init.sql b/mysql-dual-conn/init.sql index d1c2a791..9e79bb18 100644 --- a/mysql-dual-conn/init.sql +++ b/mysql-dual-conn/init.sql @@ -8,3 +8,33 @@ CREATE USER IF NOT EXISTS 'stagebuster'@'%' IDENTIFIED BY 'camundaPassword'; GRANT ALL PRIVILEGES ON camunda.* TO 'stagebuster'@'%'; FLUSH PRIVILEGES; + +-- Column-type fidelity fixture (keploy/keploy#4426). +-- +-- The OMS datasource runs with useServerPrepStmts=true, so a SELECT over +-- this table comes back as a binary-protocol result set — the wire format +-- whose FLOAT/DOUBLE columns keploy decoded as their raw IEEE-754 bit +-- pattern rather than their value, corrupting mocks.yaml at record time. +-- +-- The BIGINT UNSIGNED column covers the other half: values above MaxInt64 +-- have no lossless float64 form, so a mock format that routes them through +-- one collapses distinct rows onto the same number. +USE myntra_oms; + +CREATE TABLE IF NOT EXISTS numeric_fidelity ( + id INT PRIMARY KEY, + label VARCHAR(32) NOT NULL, + price_f FLOAT NOT NULL, + ratio_d DOUBLE NOT NULL, + big_u BIGINT UNSIGNED NOT NULL +); + +INSERT INTO numeric_fidelity (id, label, price_f, ratio_d, big_u) VALUES + -- 9.99 is the value from the bug report: read as a numeric cast it + -- surfaces as 1.0926057e+09 (FLOAT) / 4.621813488089437e+18 (DOUBLE). + (1, 'nine-ninety-nine', 9.99, 9.99, 18446744073709551615), + (2, 'negative', -0.5, -1234.5678, 9223372036854775808), + (3, 'zero', 0, 0, 0), + (4, 'whole', 10, 10, 4294967296), + (5, 'small', 1.5, 2.2250738585072014e-308, 1) +ON DUPLICATE KEY UPDATE label = VALUES(label); diff --git a/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java index 8802d0b2..d57cfdb6 100644 --- a/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java +++ b/mysql-dual-conn/src/main/java/com/example/mysqlreplicate/QueryController.java @@ -72,6 +72,51 @@ public List> queryCamunda() { * cascade into "Connection closing due to no matching mock found" and * tear down the TCP connection. */ + /** + * Selects FLOAT / DOUBLE / BIGINT UNSIGNED columns over the OMS + * datasource, which runs with useServerPrepStmts=true. + * + * The {@code id >= ?} predicate is load-bearing, not filler: without + * a bound parameter JdbcTemplate issues a plain Statement, which + * Connector/J sends as COM_QUERY and MySQL answers with a *text* + * result set — every value a length-encoded string, which is not the + * code path this fixture exists to cover. The parameter forces a + * server-side prepared statement, so the rows come back as a + * binary-protocol result set carrying raw IEEE-754 bytes. + * + * This is the read path for keploy/keploy#4426: keploy decoded the + * FLOAT and DOUBLE wire bytes as a numeric cast instead of an + * IEEE-754 reinterpret, so a column holding 9.99 was recorded as + * 1.0926057e+09 / 4.621813488089437e+18. The corruption happened at + * record time, so it survived re-recording and replay asserted + * against a value the database never returned. + * + * big_u covers the neighbouring defect: a BIGINT UNSIGNED above + * MaxInt64 has no lossless float64 form, so any mock format that + * routes it through one collapses distinct rows onto one number. + */ + @GetMapping("/api/oms/numerics") + public List> numerics() { + return omsJdbc.queryForList( + "SELECT id, label, price_f, ratio_d, big_u FROM numeric_fidelity " + + "WHERE id >= ? ORDER BY id", + 0); + } + + /** + * Binds a FLOAT parameter, exercising the COM_STMT_EXECUTE decode + * path rather than the result-set one. The bound value is a float32 + * on the wire and comes back out of the mock file as a float64, so + * keploy's parameter matcher has to compare the two at float32 + * precision — widening instead means a correctly recorded FLOAT + * parameter never matches itself and replay finds no mock. + */ + @GetMapping("/api/oms/float-param/{v}") + public List> floatParam(@PathVariable("v") float v) { + return omsJdbc.queryForList( + "SELECT id, label, price_f FROM numeric_fidelity WHERE price_f = ? ORDER BY id", v); + } + @GetMapping("/api/oms/stmt-reset/{n}") public List stmtReset(@PathVariable("n") int n) { return omsJdbc.execute((java.sql.Connection conn) -> {