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 d071a5d..c2e8a3a 100644 --- a/docs/test-results/2026-09-26-type-connection-matrix.md +++ b/docs/test-results/2026-09-26-type-connection-matrix.md @@ -56,4 +56,8 @@ ## 后续修复:GB18030 大字段边界 -新增本机官方 ARM DM8 的 GB18030(`UNICODE=0`)隔离实例。修复前,包含中文和四字节 emoji 的 CLOB 跨越 16,000 字节写入边界时,文本读回虽完整,数据库的 `LENGTH(c)` 和 `DBMS_LOB.GETLENGTH(c)` 会少计 1 至 2 个字符。写入时现在让 GB18030 数据块结束在完整字符处。Python 3.10 在 UTF-8(`UNICODE=1`)和 GB18030 两套实例上的完整真实库回归各为 **166 passed、0 skipped**,另各有 2 个非真实库用例未选入。CI 增加 Python 3.9 至 3.13 × 两种编码的矩阵,并校验数据库实际 `UNICODE` 值;CI 结果待核实。 +新增本机官方 ARM DM8 的 GB18030(`UNICODE=0`)隔离实例。修复前,包含中文和四字节 emoji 的 CLOB 跨越 16,000 字节写入边界时,文本读回虽完整,数据库的 `LENGTH(c)` 和 `DBMS_LOB.GETLENGTH(c)` 会少计 1 至 2 个字符。写入时现在让 GB18030 数据块结束在完整字符处。Python 3.10 在 UTF-8(`UNICODE=1`)和 GB18030 两套实例上的完整真实库回归各为 **166 passed、0 skipped**,另各有 2 个非真实库用例未选入。CI 增加 Python 3.9 至 3.13 × 两种编码的矩阵,并校验数据库实际 `UNICODE` 值;PR #17 的 10 个真实数据库任务和 5 个 ARM64 构建任务均已通过。 + +## 后续修复:本地时区时间戳绑定 + +`TIMESTAMP WITH LOCAL TIME ZONE` 列原先拒绝带时区的 Python `datetime`,返回 6015 日期时间格式错误。原因是 Python 默认格式把 `+05:30` 等偏移量紧贴时间,而该列要求偏移量前有空格。现在只在绑定 Python 带时区时间对象时规范化格式;正负偏移量与跨日输入均通过。Python 3.10 在 UTF-8 和 GB18030 官方 ARM DM8 实例上的完整真实库回归各为 **168 passed、0 skipped**,另各有 2 个非真实库用例未选入。 diff --git a/src/native/vDateTime.c b/src/native/vDateTime.c index b4cac1d..6e90c6e 100644 --- a/src/native/vDateTime.c +++ b/src/native/vDateTime.c @@ -530,9 +530,30 @@ TZVar_SetValue( if (PyDateTime_Check(value) || PyTime_Check(value)) { + Py_ssize_t text_len; text = PyObject_Str(value); if (!text) return -1; + + /* DM8 local time zone columns require a space before +/-HH:MM. */ + text_len = PyUnicode_GetLength(text); + if (text_len >= 6 && + (PyUnicode_ReadChar(text, text_len - 6) == '+' || + PyUnicode_ReadChar(text, text_len - 6) == '-') && + PyUnicode_ReadChar(text, text_len - 3) == ':') + { + PyObject* prefix = PyUnicode_Substring(text, 0, text_len - 6); + PyObject* offset = PyUnicode_Substring(text, text_len - 6, text_len); + PyObject* spaced = NULL; + if (prefix && offset) + spaced = PyUnicode_FromFormat("%U %U", prefix, offset); + Py_XDECREF(prefix); + Py_XDECREF(offset); + Py_DECREF(text); + if (!spaced) + return -1; + text = spaced; + } } if (dmBuffer_FromObject(&buffer, text, var->environment->encoding) < 0) diff --git a/tests/integration/test_p1_type_matrix.py b/tests/integration/test_p1_type_matrix.py index 7c5b5b2..6d6dfa3 100644 --- a/tests/integration/test_p1_type_matrix.py +++ b/tests/integration/test_p1_type_matrix.py @@ -239,6 +239,35 @@ def test_timezone_time_roundtrip_preserves_instant( cur.close() +@pytest.mark.parametrize( + "value", + [ + dt.datetime(2024, 2, 29, 23, 59, 58, 123456, tzinfo=_TZ_PLUS_0530), + dt.datetime(2024, 1, 1, 0, 0, 1, 1, tzinfo=_TZ_MINUS_1259), + ], + ids=["positive-offset", "negative-offset"], +) +def test_local_timezone_timestamp_accepts_aware_datetime( + conn, table_name_factory, drop_table, value +): + table = table_name_factory("DMPY_LOCAL_TZ") + offset = value.strftime("%z") + spaced_text = f"{value:%Y-%m-%d %H:%M:%S.%f} {offset[:3]}:{offset[3:]}" + cur = conn.cursor() + try: + cur.execute(f"CREATE TABLE {table} (id INT, v TIMESTAMP WITH LOCAL TIME ZONE)") + cur.execute(f"INSERT INTO {table} VALUES (?, ?)", (1, value)) + cur.execute(f"INSERT INTO {table} VALUES (?, ?)", (2, spaced_text)) + conn.commit() + cur.execute(f"SELECT id, v FROM {table} ORDER BY id") + rows = cur.fetchall() + assert rows[0][1] == rows[1][1] + finally: + drop_table(cur, table) + conn.commit() + cur.close() + + @pytest.mark.parametrize( "value", [