diff --git a/docs/test-results/2026-09-26-type-connection-matrix.md b/docs/test-results/2026-09-26-type-connection-matrix.md index 34a1540..09c8165 100644 --- a/docs/test-results/2026-09-26-type-connection-matrix.md +++ b/docs/test-results/2026-09-26-type-connection-matrix.md @@ -20,11 +20,14 @@ ## 已发现的问题 -1. **高精度十进制参数丢失小数**:向 `DECIMAL(30,8)` 绑定 `Decimal('12345678901234567890.12345678')` 或相同文本后,数据库中实际值为 `12345678901234567890.00000000`;直接在 SQL 中 `CAST` 同一字面量能保留小数。这是写入路径的准确性问题,当前 104 项通过不能代表高精度 `DECIMAL` 安全。修复后应把该值加入 CI 的往返断言。 -2. **连接选项的生效尚未验证**:`connection_timeout=5`、`login_timeout=5` 和 `app_name='dmpython_matrix'` 建连后,当前属性读回分别为 `0`、`0` 和空字符串。现有用例对前两项只验证“可建连”,不声称超时配置生效。需要单独设计可控的网络故障与服务器元数据检查。 +1. **连接选项的生效尚未验证**:`connection_timeout=5`、`login_timeout=5` 和 `app_name='dmpython_matrix'` 建连后,当前属性读回分别为 `0`、`0` 和空字符串。现有用例对前两项只验证“可建连”,不声称超时配置生效。需要单独设计可控的网络故障与服务器元数据检查。 本次修复了 `datetime.time` 绑定在亚洲/上海本地时区下偏移五分钟的问题:桥接层曾以公元 0 年构造无时区的 `TIME`,触发历史时区偏移;改用现代锚定日期后,上述五版用例均验证 `23:59:58` 原样写入和读回。 +## 后续修复:高精度十进制写入 + +`DECIMAL(30,8)` 参数原先经过默认精度的二进制浮点转换,使 `12345678901234567890.12345678` 的小数位归零。现在直接解析十进制字符,并覆盖 `Decimal`、文本、负数和科学计数法四种参数形式。本机官方 DM8 上 Python 3.10 的完整真实库回归为 **108 passed、0 failed、0 skipped**;尚需以 CI 结果验证托管镜像。 + ## 下一轮边界 -需要继续覆盖高精度/负数/指数形式十进制,带时区时间、区间、复杂对象与数组、BFILE、不同编码,以及 SSL、UKey、MPP、读写分离和超时/故障转移的实际效果。当前仅有一版官方 DM8 服务端和一版 GitHub CI 开发镜像的历史基线;不能据此推断跨达梦服务端版本兼容。 +需要继续覆盖其他十进制边界、带时区时间、区间、复杂对象与数组、BFILE、不同编码,以及 SSL、UKey、MPP、读写分离和超时/故障转移的实际效果。当前仅有一版官方 DM8 服务端和一版 GitHub CI 开发镜像的历史基线;不能据此推断跨达梦服务端版本兼容。 diff --git a/dpi_bridge/third_party/chunanyong_dm/o.go b/dpi_bridge/third_party/chunanyong_dm/o.go index 44c1adf..06478e6 100644 --- a/dpi_bridge/third_party/chunanyong_dm/o.go +++ b/dpi_bridge/third_party/chunanyong_dm/o.go @@ -119,11 +119,67 @@ func (d DmDecimal) ToBigFloat() *big.Float { } func NewDecimalFromString(s string) (*DmDecimal, error) { - num, ok := new(big.Float).SetString(strings.TrimSpace(s)) - if !ok { + return newDecimalFromString(s, 0, 0) +} + +func newDecimalFromString(s string, prec, scale int) (*DmDecimal, error) { + s = strings.TrimSpace(s) + sign := 1 + if strings.HasPrefix(s, "-") { + sign = -1 + s = s[1:] + } else if strings.HasPrefix(s, "+") { + s = s[1:] + } + + exponent := 0 + if i := strings.IndexAny(s, "eE"); i >= 0 { + var err error + exponent, err = strconv.Atoi(s[i+1:]) + if err != nil || exponent < -1000 || exponent > 1000 { + return nil, ECGO_DATA_CONVERTION_ERROR.throw() + } + s = s[:i] + } + + point := strings.IndexByte(s, '.') + fractionalDigits := 0 + if point >= 0 { + fractionalDigits = len(s) - point - 1 + s = s[:point] + s[point+1:] + } + if len(s) == 0 { return nil, ECGO_DATA_CONVERTION_ERROR.throw() } - return NewDecimalFromBigFloat(num) + for i := range s { + if s[i] < '0' || s[i] > '9' { + return nil, ECGO_DATA_CONVERTION_ERROR.throw() + } + } + + d := &DmDecimal{sign: sign, weight: exponent - fractionalDigits, prec: prec, scale: scale, Valid: true} + d.digits = strings.TrimLeft(s, "0") + if d.digits == "" { + d.sign = 0 + return d, nil + } + trimmed := len(d.digits) - len(strings.TrimRight(d.digits, "0")) + d.digits = strings.TrimRight(d.digits, "0") + d.weight += trimmed + if len(d.digits) > XDEC_MAX_PREC { + return nil, ECGO_DATA_TOO_LONG.throw() + } + if isOdd(d.weight) { + d.digits += "0" + d.weight-- + } + if isOdd(len(d.digits)) { + d.digits = "0" + d.digits + } + if len(d.digits) > XDEC_MAX_PREC { + return nil, ECGO_DATA_TOO_LONG.throw() + } + return d, nil } func (d DmDecimal) String() string { diff --git a/dpi_bridge/third_party/chunanyong_dm/zi.go b/dpi_bridge/third_party/chunanyong_dm/zi.go index 46562ea..5576799 100644 --- a/dpi_bridge/third_party/chunanyong_dm/zi.go +++ b/dpi_bridge/third_party/chunanyong_dm/zi.go @@ -410,7 +410,13 @@ func (G2DB g2db) fromString(val string, param parameter, conn *DmConnection) (in } z, _ := f.Int(nil) return G2DB.fromBigInt(z, param, conn) - case REAL, DOUBLE, DECIMAL: + case DECIMAL: + d, err := newDecimalFromString(val, int(param.prec), int(param.scale)) + if err != nil { + return nil, err + } + return d.encodeDecimal() + case REAL, DOUBLE: f, ok := new(big.Float).SetString(val) if ok { return G2DB.fromBigFloat(f, param, conn) diff --git a/tests/integration/test_p1_type_matrix.py b/tests/integration/test_p1_type_matrix.py index d48a5b7..12c4032 100644 --- a/tests/integration/test_p1_type_matrix.py +++ b/tests/integration/test_p1_type_matrix.py @@ -73,3 +73,30 @@ def test_typed_null_roundtrip(conn, table_name_factory, drop_table, sql_type): drop_table(cur, table) conn.commit() cur.close() + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (Decimal("12345678901234567890.12345678"), Decimal("12345678901234567890.12345678")), + ("12345678901234567890.12345678", Decimal("12345678901234567890.12345678")), + (Decimal("-12345678901234567890.12345678"), Decimal("-12345678901234567890.12345678")), + ("1234567890123456789012345678E-8", Decimal("12345678901234567890.12345678")), + ], + ids=["decimal", "text", "negative", "scientific"], +) +def test_high_precision_decimal_parameter_preserves_fraction( + conn, table_name_factory, drop_table, value, expected +): + table = table_name_factory("DMPY_DECIMAL") + cur = conn.cursor() + try: + cur.execute(f"CREATE TABLE {table} (v DECIMAL(30, 8))") + cur.execute(f"INSERT INTO {table} VALUES (?)", (value,)) + conn.commit() + cur.execute(f"SELECT CAST(v AS VARCHAR(80)) FROM {table}") + assert Decimal(cur.fetchone()[0]) == expected + finally: + drop_table(cur, table) + conn.commit() + cur.close()