Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dmPython 是达梦数据库(DM8)的原生 Python 驱动程序,遵循 [Pyth
- **Best-effort(尽力支持)**:尚未纳入 CI 覆盖的扩展使用场景。
- **Not guaranteed(不保证)**:生产 SLA 承诺、厂商认证兼容性与闭源组件支持协议。
- **连接安全**:`ssl_path` 支持使用客户端证书与私钥连接启用加密的 DM8。目录需包含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem`;服务端证书没有 SAN 的旧版本还需提供与服务端完全一致的 `server-cert.pem`。指定 `ssl_path` 时,未协商加密的连接会报错。非空的 `ssl_pwd`、`ukey_name`、`ukey_pin` 仍不支持。
- **主备路由**:读写分离模式 1 和 4 已在本机 DM8 主备环境、自动提交模式下验证;通过双端点服务名建立的新连接也通过了自动接管后的回归。MPP 集群路由仍需对应环境回归。
- **主备与 MPP**:读写分离模式 1 和 4 已在本机 DM8 主备环境、自动提交模式下验证;通过双端点服务名建立的新连接也通过了自动接管后的回归。MPP 全局和本地登录已在本机两节点集群验证分布式读写。

## 路线图与状态

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ dmPython.connect(
- `host` 与 `server` 互斥(只允许设置一个)。
- IPv6 地址使用方括号,例如 `server="[::1]"`;`dsn` 可写为 `"[::1]:5236"`。
- `dmsvc_path` 指向包含 `dm_svc.conf` 的目录;连接时可把 `server` 设为配置文件中的服务名。双端点服务名配置 `LOGIN_MODE=1` 后,已在本机 DM8 主备环境验证故障自动接管后的**新连接**会选择晋升的新主库;已有连接的自动恢复尚未验证。
- `mpp_login` 接受 `DSQL_MPP_LOGIN_GLOBAL` 或 `DSQL_MPP_LOGIN_LOCAL`;`rwseparate` 接受 `DSQL_RWSEPARATE_OFF`、`DSQL_RWSEPARATE_ON` 或 `DSQL_RWSEPARATE_ON2`,`rwseparate_percent` 范围为 0–100。这些选项在建连时传给底层驱动。读写分离模式 1 和 4 已在本机 DM8 主备环境中验证自动提交模式下的查询路由;MPP 只验证了单机 LOCAL 握手,集群路由尚待验证。
- `mpp_login` 接受 `DSQL_MPP_LOGIN_GLOBAL` 或 `DSQL_MPP_LOGIN_LOCAL`;`rwseparate` 接受 `DSQL_RWSEPARATE_OFF`、`DSQL_RWSEPARATE_ON` 或 `DSQL_RWSEPARATE_ON2`,`rwseparate_percent` 范围为 0–100。这些选项在建连时传给底层驱动。读写分离模式 1 和 4 已在本机 DM8 主备环境中验证自动提交模式下的查询路由;MPP 全局和本地登录已在本机两节点集群验证分布式读写。
- `ssl_path` 指向含 `ca-cert.pem`、`client-cert.pem`、`client-key.pem` 的目录。服务端证书没有 SAN 时还需提供准确的 `server-cert.pem`,用于证书固定校验;有 SAN 的证书按 CA 链和主机名校验。设置后若服务端未协商加密,连接失败。非空的 `ssl_pwd`、`ukey_name`、`ukey_pin` 暂不支持。
- `user` 支持 `user/password@server:port[/schema][?catalog=...]` 形式。
- `login_timeout` 以毫秒为单位,默认 5000,限制首次建连握手;设为 0 表示不限制。`connection_timeout` 以秒为单位,默认 0 不限制,限制 SQL 执行时间。
Expand Down
31 changes: 31 additions & 0 deletions docs/test-results/2026-09-27-mpp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Real DM8 MPP login and distributed decimal regression (2026-09-27)

An isolated two-node DM8 MPP cluster ran in local Orb with the 2025-09-24
ARM image. Each node had a separately initialized database (`EP01`, `EP02`),
matching database creation parameters, `MAL_INI=1`, `MPP_INI=1`, a shared
`dmmal.ini`, and the same `dmmpp.ctl` generated from `dmmpp.ini`. Both nodes
reached `SYSTEM IS READY`, and their logs showed MAL links to each other.
This follows the [DM MPP configuration model](https://eco.dameng.com/document/dm/zh-cn/pm/basic-concepts-principles.html).

The ARM macOS Python 3.10 extension connected to both endpoints with
`mpp_login=DSQL_MPP_LOGIN_GLOBAL` and `mpp_login=DSQL_MPP_LOGIN_LOCAL`.
`scripts/verify_dm_mpp.py` created a hash-distributed table, batch-inserted
12 `DECIMAL(30,8)` rows, and checked the exact values from both endpoints.
The values included a 20-digit integer part with eight fractional digits,
negative values, and `0.00000001`.

| Query | EP01 endpoint | EP02 endpoint |
| --- | ---: | ---: |
| Global login | 12 exact rows | 12 exact rows |
| Local login | 6 exact rows | 6 exact rows |

The two local row ID sets were disjoint and their union equaled the 12 global
rows, proving that the test exercised both physical EPs. The script removed
its test table after success. It takes `DM_MPP_USER`, `DM_MPP_PASSWORD`,
`DM_MPP_EP1_HOST`, and `DM_MPP_EP2_HOST`; optional endpoint ports default to
5236. The account needs permission to create and drop a distributed table.

The test was run locally because GitHub-hosted runners cannot reach the Orb
cluster. The existing CI matrix uses one DM8 instance and does not exercise
MPP. This result covers a two-EP cluster and one DM8 server build; larger
clusters and MPP primary/standby combinations remain untested.
89 changes: 89 additions & 0 deletions scripts/verify_dm_mpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Verify local and global login against a real two-node DM MPP cluster.

Set DM_MPP_USER, DM_MPP_PASSWORD, DM_MPP_EP1_HOST, and DM_MPP_EP2_HOST.
DM_MPP_EP1_PORT and DM_MPP_EP2_PORT default to 5236. The account must be
able to create and drop a distributed table.
"""

from __future__ import annotations

import os
import uuid
from decimal import Decimal

import dmPython


HIGH = Decimal("12345678901234567890.12345678")


def connect(host: str, port: int, mode: int):
return dmPython.connect(
user=os.environ["DM_MPP_USER"],
password=os.environ["DM_MPP_PASSWORD"],
server=host,
port=port,
mpp_login=mode,
autoCommit=dmPython.DSQL_AUTOCOMMIT_ON,
login_timeout=3000,
)


def rows(conn, table: str):
with conn.cursor() as cur:
cur.execute(f"SELECT ID, CAST(AMOUNT AS VARCHAR(80)) FROM {table} ORDER BY ID")
return {row_id: Decimal(amount) for row_id, amount in cur.fetchall()}


def local_instance(conn):
with conn.cursor() as cur:
cur.execute("SELECT INSTANCE_NAME FROM V$INSTANCE")
return cur.fetchone()[0]


def main():
endpoints = (
(os.environ["DM_MPP_EP1_HOST"], int(os.environ.get("DM_MPP_EP1_PORT", "5236"))),
(os.environ["DM_MPP_EP2_HOST"], int(os.environ.get("DM_MPP_EP2_PORT", "5236"))),
)
table = "DMPY_MPP_" + uuid.uuid4().hex[:8].upper()
expected = {i: HIGH if i % 2 else -HIGH for i in range(1, 13)}
expected[12] = Decimal("0.00000001")

with connect(*endpoints[0], dmPython.DSQL_MPP_LOGIN_GLOBAL) as global_conn:
with global_conn.cursor() as cur:
cur.execute(
f"CREATE TABLE {table} (ID INT PRIMARY KEY, AMOUNT DECIMAL(30,8)) "
"DISTRIBUTED BY HASH(ID)"
)
try:
with global_conn.cursor() as cur:
cur.executemany(
f"INSERT INTO {table} VALUES (?, ?)", list(expected.items())
)

local_rows = []
instance_names = []
for host, port in endpoints:
with connect(host, port, dmPython.DSQL_MPP_LOGIN_GLOBAL) as conn:
assert rows(conn, table) == expected
with connect(host, port, dmPython.DSQL_MPP_LOGIN_LOCAL) as conn:
instance_names.append(local_instance(conn))
local_rows.append(rows(conn, table))

assert instance_names[0] != instance_names[1], instance_names
assert local_rows[0] and local_rows[1], "both EPs must store rows"
assert local_rows[0].keys().isdisjoint(local_rows[1].keys())
assert local_rows[0] | local_rows[1] == expected
print(
f"MPP global: {len(expected)} exact rows from both endpoints; "
f"local: {instance_names[0]}={len(local_rows[0])}, "
f"{instance_names[1]}={len(local_rows[1])}"
)
finally:
with global_conn.cursor() as cur:
cur.execute(f"DROP TABLE {table}")


if __name__ == "__main__":
main()
Loading