From 92acea7541a3fa447464db296d9f348f177eeea6 Mon Sep 17 00:00:00 2001 From: skhe Date: Fri, 6 Mar 2026 14:10:50 +0800 Subject: [PATCH 1/5] docs: add full API reference, runnable examples, and pages CI --- .github/workflows/docs-pages.yml | 57 +++++ README.md | 10 + docs/api-reference.md | 283 +++++++++++++++++++++++ docs/examples/basic-crud.md | 60 +++++ docs/examples/bulk-insert.md | 50 ++++ docs/examples/connection-pool.md | 82 +++++++ docs/examples/lob-handling.md | 62 +++++ docs/examples/scripts/basic_crud.py | 47 ++++ docs/examples/scripts/bulk_insert.py | 37 +++ docs/examples/scripts/connection_pool.py | 67 ++++++ docs/examples/scripts/lob_handling.py | 49 ++++ docs/examples/scripts/stored_proc.py | 51 ++++ docs/examples/stored-proc.md | 64 +++++ docs/faq.md | 25 ++ docs/index.md | 34 +++ docs/installation.md | 52 +++++ docs/migration.md | 50 ++++ docs/quickstart.md | 37 +++ mkdocs.yml | 27 +++ 19 files changed, 1144 insertions(+) create mode 100644 .github/workflows/docs-pages.yml create mode 100644 docs/api-reference.md create mode 100644 docs/examples/basic-crud.md create mode 100644 docs/examples/bulk-insert.md create mode 100644 docs/examples/connection-pool.md create mode 100644 docs/examples/lob-handling.md create mode 100644 docs/examples/scripts/basic_crud.py create mode 100644 docs/examples/scripts/bulk_insert.py create mode 100644 docs/examples/scripts/connection_pool.py create mode 100644 docs/examples/scripts/lob_handling.py create mode 100644 docs/examples/scripts/stored_proc.py create mode 100644 docs/examples/stored-proc.md create mode 100644 docs/faq.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/migration.md create mode 100644 docs/quickstart.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml new file mode 100644 index 0000000..9fd4634 --- /dev/null +++ b/.github/workflows/docs-pages.yml @@ -0,0 +1,57 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install docs dependencies + run: | + python -m pip install --upgrade pip + pip install mkdocs + + - name: Build docs + run: mkdocs build --strict + + - name: Setup Pages + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 1bdc149..30c3a8e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,16 @@ cursor.close() conn.close() ``` +## Documentation + +- GitHub Pages: https://skhe.github.io/dmPython/ +- Local preview: + +```bash +pip install mkdocs +mkdocs serve +``` + ## Building from Source **Prerequisites:** diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..bf2161d --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,283 @@ +# API 参考 + +本页根据扩展源码 `src/native/py_Dameng.c`、`src/native/Connection.c`、`src/native/Cursor.c` 汇总公开接口。 + +## 模块级对象 + +### DB-API 元信息 + +- `dmPython.apilevel = "2.0"` +- `dmPython.threadsafety = 1` +- `dmPython.paramstyle = "qmark"` +- `dmPython.version` +- `dmPython.buildtime` + +### 连接入口 + +- `dmPython.connect(...)` +- `dmPython.Connect(...)` + +两者均为 `Connection` 类型构造入口。 + +### `connect()` 参数 + +```python +dmPython.connect( + user=None, + password=None, + dsn=None, + host=None, + server=None, + port=None, + access_mode=None, + autoCommit=None, + connection_timeout=None, + login_timeout=None, + txn_isolation=None, + app_name=None, + compress_msg=None, + use_stmt_pool=None, + ssl_path=None, + ssl_pwd=None, + mpp_login=None, + ukey_name=None, + ukey_pin=None, + rwseparate=None, + rwseparate_percent=None, + cursor_rollback_behavior=None, + lang_id=None, + local_code=None, + cursorclass=None, + schema=None, + shake_crypto=None, + catalog=None, + dmsvc_path=None, + parse_type=None, +) +``` + +说明: + +- `host` 与 `server` 互斥(只允许设置一个)。 +- `user` 支持 `user/password@server:port[/schema][?catalog=...]` 形式。 +- 常量参数建议使用模块常量(如 `DSQL_AUTOCOMMIT_ON`、`ISO_LEVEL_READ_COMMITTED`)。 + +### 模块函数 + +- `DateFromTicks(ticks)` +- `TimeFromTicks(ticks)` +- `TimestampFromTicks(ticks)` +- `StringFromBytes(bs)` + +### 日期时间类型别名 + +- `Date` +- `Time` +- `Timestamp` +- `DATETIME` + +### 游标类型常量 + +- `TupleCursor` +- `DictCursor` + +用于 `connect(cursorclass=...)`。 + +## Connection + +### 方法 + +- `cursor()` +- `commit()` +- `rollback()` +- `close()` +- `disconnect()`(`close()` 别名) +- `debug(debug_type=dmPython.DEBUG_OPEN)` +- `shutdown(shutdown_type=dmPython.SHUTDOWN_DEFAULT)` +- `explain(statement)` +- `ping(reconnect=0)` +- `__enter__()` +- `__exit__(exc_type, exc_value, exc_traceback)` + +### 成员属性(只读) + +- `dsn` +- `server_status` +- `warning` + +### 计算属性(含可写项) + +可读写: + +- `access_mode` +- `async_enable` +- `auto_ipd` +- `local_code` +- `lang_id` +- `app_name` +- `txn_isolation` +- `compress_msg` +- `rwseparate` +- `rwseparate_percent` +- `use_stmt_pool` +- `ssl_path` +- `mpp_login` +- `autoCommit` +- `autocommit` +- `connection_dead` +- `connection_timeout` +- `login_timeout` +- `packet_size` +- `port` + +只读: + +- `server_code` +- `current_schema` +- `str_case_sensitive` +- `max_row_size` +- `current_catalog` +- `trx_state` +- `server_version` +- `cursor_rollback_behavior` +- `user` +- `server` +- `inst_name` +- `version` +- `max_identifier_length` +- `outputtypehandler` +- `stmtcachesize` + +以上属性多数存在同名 `DSQL_ATTR_*` 别名,例如: + +- `connection.autoCommit` <=> `connection.DSQL_ATTR_AUTOCOMMIT` +- `connection.port` <=> `connection.DSQL_ATTR_LOGIN_PORT` + +## Cursor + +### 方法 + +- `execute(statement, params=None, **kwargs)` +- `executedirect(statement)` +- `fetchall()` +- `fetchone()` +- `fetchmany(rows=arraysize)` +- `prepare(statement)` +- `parse(statement)`(当前实现返回 `NotSupportedError`) +- `setinputsizes(*args, **kwargs)` +- `executemany(statement, seq_of_params)` +- `callproc(name, params=None)` +- `callfunc(name, params=None)` +- `setoutputsize(size, column=-1)` +- `var(typ, size=0, arraysize=cursor.arraysize, inconverter=None, outconverter=None, typename=None, encoding_errors=None, bypass_decode=False, encodingErrors=None)` +- `arrayvar(...)`(当前实现返回 `NotSupportedError`) +- `bindnames()`(当前实现返回 `NotSupportedError`) +- `close()` +- `next()` +- `nextset()` +- `__enter__()` +- `__exit__(exc_type, exc_value, exc_traceback)` + +### 成员属性 + +- `arraysize`(可写) +- `bindarraysize`(可写) +- `rowcount`(只读) +- `rownumber`(只读) +- `with_rows`(只读) +- `statement`(只读) +- `connection`(只读) +- `column_names`(只读) +- `lastrowid`(只读) +- `execid`(只读) +- `_isClosed`(内部) +- `_statement`(内部) +- `output_stream`(可写) +- `description`(只读计算属性) + +## 异常层次 + +- `Warning` +- `Error` + - `InterfaceError` + - `DatabaseError` + - `DataError` + - `OperationalError` + - `IntegrityError` + - `InternalError` + - `ProgrammingError` + - `NotSupportedError` + +此外还提供 `DmError` 对象(包含 `code`、`offset`、`message`、`context`)。 + +## 常量 + +### 调试与关库 + +- `DEBUG_CLOSE` +- `DEBUG_OPEN` +- `DEBUG_SWITCH` +- `DEBUG_SIMPLE` +- `SHUTDOWN_DEFAULT` +- `SHUTDOWN_ABORT` +- `SHUTDOWN_IMMEDIATE` +- `SHUTDOWN_TRANSACTIONAL` +- `SHUTDOWN_NORMAL` + +### 事务与访问模式 + +- `ISO_LEVEL_READ_DEFAULT` +- `ISO_LEVEL_READ_UNCOMMITTED` +- `ISO_LEVEL_READ_COMMITTED` +- `ISO_LEVEL_REPEATABLE_READ` +- `ISO_LEVEL_SERIALIZABLE` +- `DSQL_MODE_READ_ONLY` +- `DSQL_MODE_READ_WRITE` +- `DSQL_AUTOCOMMIT_ON` +- `DSQL_AUTOCOMMIT_OFF` + +### 编码与语言 + +- `PG_UTF8` +- `PG_GBK` +- `PG_BIG5` +- `PG_ISO_8859_9` +- `PG_EUC_JP` +- `PG_EUC_KR` +- `PG_KOI8R` +- `PG_ISO_8859_1` +- `PG_SQL_ASCII` +- `PG_GB18030` +- `PG_ISO_8859_11` +- `LANGUAGE_CN` +- `LANGUAGE_EN` +- `LANGUAGE_CNT_HK`(条件编译) + +### 其他连接行为 + +- `DSQL_TRUE` +- `DSQL_FALSE` +- `DSQL_RWSEPARATE_ON` +- `DSQL_RWSEPARATE_OFF` +- `DSQL_TRX_ACTIVE` +- `DSQL_TRX_COMPLETE` +- `DSQL_MPP_LOGIN_GLOBAL` +- `DSQL_MPP_LOGIN_LOCAL` +- `DSQL_CB_CLOSE` +- `DSQL_CB_PRESERVE` + +### 数据类型对象 + +模块还导出一组数据类型对象,可用于绑定/类型判断: + +- `INTERVAL`, `YEAR_MONTH_INTERVAL` +- `BLOB`, `CLOB`, `LOB` +- `BFILE`, `exBFILE` +- `LONG_BINARY`, `LONG_STRING` +- `DATE`, `TIME`, `TIMESTAMP` +- `CURSOR` +- `STRING`, `FIXED_STRING`, `BINARY`, `FIXED_BINARY` +- `OBJECTVAR`, `objectvar` +- `NUMBER`, `DOUBLE`, `REAL`, `BOOLEAN`, `DECIMAL` +- `TIME_WITH_TIMEZONE`, `TIMESTAMP_WITH_TIMEZONE` +- `BIGINT`, `ROWID` diff --git a/docs/examples/basic-crud.md b/docs/examples/basic-crud.md new file mode 100644 index 0000000..b1f8a7c --- /dev/null +++ b/docs/examples/basic-crud.md @@ -0,0 +1,60 @@ +# 基本增删改查 + +## 运行方式 + +```bash +python docs/examples/scripts/basic_crud.py +``` + +## 示例代码 + +```python +# docs/examples/scripts/basic_crud.py +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + table = "SKH70_BASIC_CRUD" + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, name VARCHAR(100), score INT)") + + cur.execute(f"INSERT INTO {table}(id, name, score) VALUES (?, ?, ?)", (1, "alice", 88)) + conn.commit() + + cur.execute(f"SELECT id, name, score FROM {table} WHERE id = ?", (1,)) + print("after insert:", cur.fetchone()) + + cur.execute(f"UPDATE {table} SET score = ? WHERE id = ?", (95, 1)) + conn.commit() + + cur.execute(f"SELECT id, name, score FROM {table} WHERE id = ?", (1,)) + print("after update:", cur.fetchone()) + + cur.execute(f"DELETE FROM {table} WHERE id = ?", (1,)) + conn.commit() + + cur.execute(f"SELECT COUNT(*) FROM {table}") + print("after delete:", cur.fetchone()) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() +``` diff --git a/docs/examples/bulk-insert.md b/docs/examples/bulk-insert.md new file mode 100644 index 0000000..2fc656e --- /dev/null +++ b/docs/examples/bulk-insert.md @@ -0,0 +1,50 @@ +# 批量插入 + +## 运行方式 + +```bash +python docs/examples/scripts/bulk_insert.py +``` + +## 示例代码 + +```python +# docs/examples/scripts/bulk_insert.py +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + table = "SKH70_BULK" + rows = [(i, f"user_{i}", i * 10) for i in range(1, 1001)] + + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, name VARCHAR(100), score INT)") + + cur.executemany(f"INSERT INTO {table}(id, name, score) VALUES (?, ?, ?)", rows) + conn.commit() + + cur.execute(f"SELECT COUNT(*) FROM {table}") + print("inserted rows:", cur.fetchone()[0]) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() +``` diff --git a/docs/examples/connection-pool.md b/docs/examples/connection-pool.md new file mode 100644 index 0000000..6a8607c --- /dev/null +++ b/docs/examples/connection-pool.md @@ -0,0 +1,82 @@ +# 连接池 + +驱动本身没有独立 `SessionPool` 类型时,可以在应用层实现简单连接池。 + +## 运行方式 + +```bash +python docs/examples/scripts/connection_pool.py +``` + +## 示例代码 + +```python +# docs/examples/scripts/connection_pool.py +import os +from contextlib import contextmanager +from queue import LifoQueue + +import dmPython + + +class SimpleConnectionPool: + def __init__(self, min_size: int = 1, max_size: int = 5): + self.min_size = min_size + self.max_size = max_size + self._created = 0 + self._idle = LifoQueue(maxsize=max_size) + for _ in range(min_size): + self._idle.put(self._new_conn()) + + def _conn_params(self): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + def _new_conn(self): + self._created += 1 + return dmPython.connect(**self._conn_params()) + + @contextmanager + def acquire(self): + conn = None + try: + if not self._idle.empty(): + conn = self._idle.get() + elif self._created < self.max_size: + conn = self._new_conn() + else: + conn = self._idle.get() + yield conn + finally: + if conn is not None: + self._idle.put(conn) + + def closeall(self): + while not self._idle.empty(): + conn = self._idle.get() + conn.close() + + +def main() -> None: + pool = SimpleConnectionPool(min_size=2, max_size=4) + + with pool.acquire() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + print("conn-1:", cur.fetchone()) + + with pool.acquire() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 2") + print("conn-2:", cur.fetchone()) + + pool.closeall() + + +if __name__ == "__main__": + main() +``` diff --git a/docs/examples/lob-handling.md b/docs/examples/lob-handling.md new file mode 100644 index 0000000..27e83cb --- /dev/null +++ b/docs/examples/lob-handling.md @@ -0,0 +1,62 @@ +# LOB 大对象操作 + +## 运行方式 + +```bash +python docs/examples/scripts/lob_handling.py +``` + +## 示例代码 + +```python +# docs/examples/scripts/lob_handling.py +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def _read_lob(val): + if hasattr(val, "read"): + return val.read() + return val + + +def main() -> None: + table = "SKH70_LOB" + text = "达梦 LOB 示例" * 500 + data = b"DMLOB" * 500 + + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, c CLOB, b BLOB)") + + cur.execute(f"INSERT INTO {table}(id, c, b) VALUES (?, ?, ?)", (1, text, data)) + conn.commit() + + cur.execute(f"SELECT c, b FROM {table} WHERE id = ?", (1,)) + c_val, b_val = cur.fetchone() + + c_content = _read_lob(c_val) + b_content = _read_lob(b_val) + print("clob length:", len(c_content)) + print("blob length:", len(b_content)) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() +``` diff --git a/docs/examples/scripts/basic_crud.py b/docs/examples/scripts/basic_crud.py new file mode 100644 index 0000000..0618333 --- /dev/null +++ b/docs/examples/scripts/basic_crud.py @@ -0,0 +1,47 @@ +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + table = "SKH70_BASIC_CRUD" + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, name VARCHAR(100), score INT)") + + cur.execute(f"INSERT INTO {table}(id, name, score) VALUES (?, ?, ?)", (1, "alice", 88)) + conn.commit() + + cur.execute(f"SELECT id, name, score FROM {table} WHERE id = ?", (1,)) + print("after insert:", cur.fetchone()) + + cur.execute(f"UPDATE {table} SET score = ? WHERE id = ?", (95, 1)) + conn.commit() + + cur.execute(f"SELECT id, name, score FROM {table} WHERE id = ?", (1,)) + print("after update:", cur.fetchone()) + + cur.execute(f"DELETE FROM {table} WHERE id = ?", (1,)) + conn.commit() + + cur.execute(f"SELECT COUNT(*) FROM {table}") + print("after delete:", cur.fetchone()) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/scripts/bulk_insert.py b/docs/examples/scripts/bulk_insert.py new file mode 100644 index 0000000..7b52450 --- /dev/null +++ b/docs/examples/scripts/bulk_insert.py @@ -0,0 +1,37 @@ +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + table = "SKH70_BULK" + rows = [(i, f"user_{i}", i * 10) for i in range(1, 1001)] + + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, name VARCHAR(100), score INT)") + + cur.executemany(f"INSERT INTO {table}(id, name, score) VALUES (?, ?, ?)", rows) + conn.commit() + + cur.execute(f"SELECT COUNT(*) FROM {table}") + print("inserted rows:", cur.fetchone()[0]) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/scripts/connection_pool.py b/docs/examples/scripts/connection_pool.py new file mode 100644 index 0000000..94cea83 --- /dev/null +++ b/docs/examples/scripts/connection_pool.py @@ -0,0 +1,67 @@ +import os +from contextlib import contextmanager +from queue import LifoQueue + +import dmPython + + +class SimpleConnectionPool: + def __init__(self, min_size: int = 1, max_size: int = 5): + self.min_size = min_size + self.max_size = max_size + self._created = 0 + self._idle = LifoQueue(maxsize=max_size) + for _ in range(min_size): + self._idle.put(self._new_conn()) + + def _conn_params(self): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + def _new_conn(self): + self._created += 1 + return dmPython.connect(**self._conn_params()) + + @contextmanager + def acquire(self): + conn = None + try: + if not self._idle.empty(): + conn = self._idle.get() + elif self._created < self.max_size: + conn = self._new_conn() + else: + conn = self._idle.get() + yield conn + finally: + if conn is not None: + self._idle.put(conn) + + def closeall(self): + while not self._idle.empty(): + conn = self._idle.get() + conn.close() + + +def main() -> None: + pool = SimpleConnectionPool(min_size=2, max_size=4) + + with pool.acquire() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + print("conn-1:", cur.fetchone()) + + with pool.acquire() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 2") + print("conn-2:", cur.fetchone()) + + pool.closeall() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/scripts/lob_handling.py b/docs/examples/scripts/lob_handling.py new file mode 100644 index 0000000..c623538 --- /dev/null +++ b/docs/examples/scripts/lob_handling.py @@ -0,0 +1,49 @@ +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def _read_lob(val): + if hasattr(val, "read"): + return val.read() + return val + + +def main() -> None: + table = "SKH70_LOB" + text = "达梦 LOB 示例" * 500 + data = b"DMLOB" * 500 + + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute(f"DROP TABLE IF EXISTS {table}") + cur.execute(f"CREATE TABLE {table} (id INT PRIMARY KEY, c CLOB, b BLOB)") + + cur.execute(f"INSERT INTO {table}(id, c, b) VALUES (?, ?, ?)", (1, text, data)) + conn.commit() + + cur.execute(f"SELECT c, b FROM {table} WHERE id = ?", (1,)) + c_val, b_val = cur.fetchone() + + c_content = _read_lob(c_val) + b_content = _read_lob(b_val) + print("clob length:", len(c_content)) + print("blob length:", len(b_content)) + + cur.execute(f"DROP TABLE IF EXISTS {table}") + conn.commit() + cur.close() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/scripts/stored_proc.py b/docs/examples/scripts/stored_proc.py new file mode 100644 index 0000000..badcad1 --- /dev/null +++ b/docs/examples/scripts/stored_proc.py @@ -0,0 +1,51 @@ +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute( + """ +CREATE OR REPLACE PROCEDURE SKH70_PROC(p_in INT, p_out OUT INT) +AS +BEGIN + p_out := p_in * 2; +END; +""" + ) + + cur.execute( + """ +CREATE OR REPLACE FUNCTION SKH70_FUNC(p_in INT) +RETURN INT +AS +BEGIN + RETURN p_in + 100; +END; +""" + ) + conn.commit() + + proc_result = cur.callproc("SKH70_PROC", [21, None]) + func_result = cur.callfunc("SKH70_FUNC", [23]) + + print("callproc:", proc_result) + print("callfunc:", func_result) + + cur.close() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/stored-proc.md b/docs/examples/stored-proc.md new file mode 100644 index 0000000..fe31e8b --- /dev/null +++ b/docs/examples/stored-proc.md @@ -0,0 +1,64 @@ +# 存储过程 + +## 运行方式 + +```bash +python docs/examples/scripts/stored_proc.py +``` + +## 示例代码 + +```python +# docs/examples/scripts/stored_proc.py +import os +import dmPython + + +def conn_params(): + return { + "server": os.getenv("DM_HOST", "localhost"), + "port": int(os.getenv("DM_PORT", "5236")), + "user": os.getenv("DM_USER", "SYSDBA"), + "password": os.getenv("DM_PASSWORD", "SYSDBA001"), + } + + +def main() -> None: + conn = dmPython.connect(**conn_params()) + cur = conn.cursor() + + cur.execute( + """ +CREATE OR REPLACE PROCEDURE SKH70_PROC(p_in INT, p_out OUT INT) +AS +BEGIN + p_out := p_in * 2; +END; +""" + ) + + cur.execute( + """ +CREATE OR REPLACE FUNCTION SKH70_FUNC(p_in INT) +RETURN INT +AS +BEGIN + RETURN p_in + 100; +END; +""" + ) + conn.commit() + + proc_result = cur.callproc("SKH70_PROC", [21, None]) + func_result = cur.callfunc("SKH70_FUNC", [23]) + + print("callproc:", proc_result) + print("callfunc:", func_result) + + cur.close() + conn.close() + + +if __name__ == "__main__": + main() +``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..fb1c840 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,25 @@ +# FAQ + +## 1. `host` 和 `server` 有什么区别? + +`connect()` 同时提供了 `host` 与 `server` 参数,两者语义相近,但不能同时设置。 + +## 2. `connect()` 最简参数是什么? + +至少应提供可用凭据和目标地址,常见是:`user`、`password`、`server`、`port`。 + +## 3. 支持字典游标吗? + +支持。连接时传 `cursorclass=dmPython.DictCursor`,查询结果按列名映射为字典。 + +## 4. 为什么 `Cursor.parse()` 报 `NotSupportedError`? + +这是当前实现状态,不是调用方式问题。可使用 `prepare()` 或直接 `execute()`。 + +## 5. 如何查看连接是否失效? + +可读 `connection.connection_dead` 或调用 `connection.ping(reconnect=1)`。 + +## 6. 如何调试服务端日志开关? + +使用 `connection.debug()`,参数可选 `DEBUG_OPEN/DEBUG_CLOSE/DEBUG_SWITCH/DEBUG_SIMPLE`。 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0946287 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +# dmPython-macOS 文档 + +这是一套面向 `dmPython-macOS` 的完整使用文档,覆盖: + +- 安装与环境准备 +- 快速开始 +- 完整 API 参考(`connect()`、`Connection`、`Cursor`、异常、常量) +- 可独立运行示例(CRUD、批量、LOB、存储过程、连接池) +- 从官方 `dmPython` 迁移到 macOS 版本的指南 + +## 文档导航 + +- [安装指南](installation.md) +- [快速开始](quickstart.md) +- [API 参考](api-reference.md) +- 示例 + - [基本增删改查](examples/basic-crud.md) + - [批量插入](examples/bulk-insert.md) + - [LOB 大对象操作](examples/lob-handling.md) + - [存储过程](examples/stored-proc.md) + - [连接池](examples/connection-pool.md) +- [迁移指南](migration.md) +- [常见问题](faq.md) + +## 约定 + +本文示例默认通过环境变量读取连接参数: + +- `DM_HOST` +- `DM_PORT` +- `DM_USER` +- `DM_PASSWORD` + +示例默认使用 `DM_PORT=5236`、`DM_USER=SYSDBA`、`DM_PASSWORD=SYSDBA001`。 diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..fcbca17 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,52 @@ +# 安装指南 + +## 支持矩阵 + +- Python: 3.9 - 3.13 +- 平台(本仓库发布目标): macOS ARM64 + +> 说明:本项目是官方 `dmPython` 的 macOS ARM64 社区 fork。Linux/Windows 生产环境请优先评估官方发布版本。 + +## 方式一:安装预编译 wheel(推荐) + +从 GitHub Releases 下载后安装: + +```bash +pip install dmPython_macOS--cp312-cp312-macosx_14_0_arm64.whl +``` + +## 方式二:从源码构建 + +前置条件: + +- Go 1.21+ +- Python 3.9+ +- DPI 头文件(放在 `dpi_include/` 或设置 `DM_HOME`) + +```bash +git clone https://github.com/skhe/dmPython.git +cd dmPython +python -m build --wheel +``` + +本地开发构建扩展: + +```bash +python setup.py build_ext --inplace +``` + +如果本地已有 `libdmdpi.dylib`,可跳过 Go 构建: + +```bash +DMPYTHON_SKIP_GO_BUILD=1 python -m build --wheel +``` + +## 安装验证 + +```bash +python - <<'PY' +import dmPython +print("version:", dmPython.version) +print("buildtime:", dmPython.buildtime) +PY +``` diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..384806e --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,50 @@ +# 从官方 dmPython 迁移到 macOS 版本 + +## 目标读者 + +- 当前使用官方 `dmPython`,希望在 macOS ARM64 运行 +- 代码层面尽量保持 DB-API 使用方式不变 + +## 兼容性结论 + +在多数业务代码里,迁移只需要替换安装来源,`import dmPython` 与 `connect()/cursor()/execute()` 调用方式保持一致。 + +## 迁移步骤 + +1. 卸载旧包并安装 macOS wheel + +```bash +pip uninstall -y dmPython dmPython-macOS +pip install dmPython_macOS--cp312-cp312-macosx_14_0_arm64.whl +``` + +2. 验证运行时版本 + +```bash +python - <<'PY' +import dmPython +print(dmPython.version) +print(dmPython.buildtime) +PY +``` + +3. 回归关键路径 + +- 建连与断连 +- 事务提交/回滚 +- 批量写入 +- LOB 读写 +- 存储过程调用 + +## 差异与注意事项 + +- 平台定位:本 fork 的发布目标是 macOS ARM64。 +- 底层实现:使用 Go DPI bridge 替代上游依赖的专有 `libdmdpi`。 +- 连接池:驱动本身不提供独立 `SessionPool` 对象,建议应用层连接池(见 [连接池示例](examples/connection-pool.md))。 +- 未支持接口:`Cursor.parse()`、`Cursor.arrayvar()`、`Cursor.bindnames()` 当前返回 `NotSupportedError`。 + +## 常见迁移问题 + +- `ImportError`:确认 wheel Python ABI 与本地 Python 版本匹配。 +- 建连失败:确认 `server/port/user/password`、网络连通性、数据库监听配置。 +- 字符编码问题:检查 `local_code`、`lang_id` 参数设置。 diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..9300189 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,37 @@ +# 快速开始 + +## 最小可运行示例 + +```python +import dmPython + +conn = dmPython.connect( + user="SYSDBA", + password="SYSDBA001", + server="localhost", + port=5236, +) + +cur = conn.cursor() +cur.execute("SELECT 1") +print(cur.fetchone()) + +cur.close() +conn.close() +``` + +## 使用上下文管理器 + +```python +import dmPython + +with dmPython.connect(user="SYSDBA", password="SYSDBA001", server="localhost", port=5236) as conn: + with conn.cursor() as cur: + cur.execute("SELECT SYSTIMESTAMP") + print(cur.fetchone()) +``` + +## 下一步 + +- 查看 [API 参考](api-reference.md) +- 查看 [示例目录](examples/basic-crud.md) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..af9d7e7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,27 @@ +site_name: dmPython-macOS Documentation +site_description: dmPython-macOS API reference and examples +site_url: https://skhe.github.io/dmPython/ +repo_url: https://github.com/skhe/dmPython +repo_name: skhe/dmPython + +theme: + name: mkdocs + +markdown_extensions: + - tables + - toc: + permalink: true + +nav: + - 首页: index.md + - 安装指南: installation.md + - 快速开始: quickstart.md + - API 参考: api-reference.md + - 示例: + - 基本增删改查: examples/basic-crud.md + - 批量插入: examples/bulk-insert.md + - LOB 大对象操作: examples/lob-handling.md + - 存储过程调用: examples/stored-proc.md + - 连接池: examples/connection-pool.md + - 迁移指南: migration.md + - 常见问题: faq.md From 639ad3af55563d84629d6393d1e93e4ab5591a8f Mon Sep 17 00:00:00 2001 From: skhe Date: Fri, 6 Mar 2026 14:11:59 +0800 Subject: [PATCH 2/5] chore: drop docs workflow from branch due token scope limits --- .github/workflows/docs-pages.yml | 57 -------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 .github/workflows/docs-pages.yml diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml deleted file mode 100644 index 9fd4634..0000000 --- a/.github/workflows/docs-pages.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Docs - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: docs-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install docs dependencies - run: | - python -m pip install --upgrade pip - pip install mkdocs - - - name: Build docs - run: mkdocs build --strict - - - name: Setup Pages - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} - uses: actions/configure-pages@v5 - - - name: Upload Pages artifact - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} - uses: actions/upload-pages-artifact@v3 - with: - path: site - - deploy: - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 From 5e71279baefdf95d072284539cc25ad2c53fccbc Mon Sep 17 00:00:00 2001 From: skhe Date: Fri, 6 Mar 2026 14:14:13 +0800 Subject: [PATCH 3/5] docs: keep pages workflow template under docs/ci due token limits --- docs/ci/docs-pages.workflow.yml | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/ci/docs-pages.workflow.yml diff --git a/docs/ci/docs-pages.workflow.yml b/docs/ci/docs-pages.workflow.yml new file mode 100644 index 0000000..9fd4634 --- /dev/null +++ b/docs/ci/docs-pages.workflow.yml @@ -0,0 +1,57 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install docs dependencies + run: | + python -m pip install --upgrade pip + pip install mkdocs + + - name: Build docs + run: mkdocs build --strict + + - name: Setup Pages + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 0c7220d4a7ea26e0d54033f2284945b365eef0d3 Mon Sep 17 00:00:00 2001 From: skhe Date: Fri, 6 Mar 2026 14:17:00 +0800 Subject: [PATCH 4/5] chore: add helper to install docs pages workflow template --- scripts/install_docs_workflow.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 scripts/install_docs_workflow.sh diff --git a/scripts/install_docs_workflow.sh b/scripts/install_docs_workflow.sh new file mode 100755 index 0000000..30e4152 --- /dev/null +++ b/scripts/install_docs_workflow.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC="$ROOT_DIR/docs/ci/docs-pages.workflow.yml" +DST_DIR="$ROOT_DIR/.github/workflows" +DST="$DST_DIR/docs-pages.yml" + +if [[ ! -f "$SRC" ]]; then + echo "missing template: $SRC" >&2 + exit 1 +fi + +mkdir -p "$DST_DIR" +cp "$SRC" "$DST" + +echo "installed: $DST" + +echo "next:" +echo " git add .github/workflows/docs-pages.yml" +echo " git commit -m 'ci(docs): add pages workflow'" +echo " git push" From 0ce8c63fd21337efffae631cab022b763ebbbdca Mon Sep 17 00:00:00 2001 From: skhe Date: Fri, 6 Mar 2026 14:19:19 +0800 Subject: [PATCH 5/5] ci: enforce docs structure and API reference coverage checks --- scripts/check_version_consistency.py | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/scripts/check_version_consistency.py b/scripts/check_version_consistency.py index 36184b6..71fecab 100755 --- a/scripts/check_version_consistency.py +++ b/scripts/check_version_consistency.py @@ -6,6 +6,7 @@ import re import sys from pathlib import Path +from typing import Iterable ROOT = Path(__file__).resolve().parents[1] @@ -71,6 +72,93 @@ def _check_runtime_version(version: str, strict: bool) -> None: print(f"[WARN] Runtime check skipped: {exc}") +def _extract_names_from_c_array(text: str, array_name: str) -> list[str]: + pattern = rf"static\s+Py(?:MethodDef|MemberDef|GetSetDef)\s+{re.escape(array_name)}\[\]\s*=\s*\{{(.*?)\n\}};" + match = re.search(pattern, text, re.DOTALL) + if not match: + _fail(f"Cannot find C array: {array_name}") + body = match.group(1) + body = re.sub(r"/\*.*?\*/", "", body, flags=re.DOTALL) + body = re.sub(r"//.*", "", body) + names = re.findall(r'\{\s*"([^"]+)"', body) + return [n for n in names if n != "NULL"] + + +def _extract_connect_keywords(connection_text: str) -> list[str]: + match = re.search( + r"static char \*keywordList\[\]\s*=\s*\{(.*?)NULL\s*\};", + connection_text, + re.DOTALL, + ) + if not match: + _fail("Cannot find connect() keywordList in Connection.c") + return re.findall(r'"([^"]+)"', match.group(1)) + + +def _missing_tokens(doc_text: str, tokens: Iterable[str]) -> list[str]: + missing = [t for t in tokens if t not in doc_text] + return sorted(set(missing)) + + +def _check_docs_structure() -> None: + required = [ + "docs/index.md", + "docs/installation.md", + "docs/quickstart.md", + "docs/api-reference.md", + "docs/examples/basic-crud.md", + "docs/examples/bulk-insert.md", + "docs/examples/lob-handling.md", + "docs/examples/stored-proc.md", + "docs/examples/connection-pool.md", + "docs/migration.md", + "docs/faq.md", + "mkdocs.yml", + ] + missing = [path for path in required if not (ROOT / path).exists()] + if missing: + _fail(f"Missing required docs files: {missing}") + + +def _check_api_docs_coverage() -> None: + api_doc = (ROOT / "docs/api-reference.md").read_text(encoding="utf-8") + # Native C sources contain mixed legacy comments; decode losslessly. + conn_c = (ROOT / "src/native/Connection.c").read_text(encoding="latin-1") + cur_c = (ROOT / "src/native/Cursor.c").read_text(encoding="latin-1") + mod_c = (ROOT / "src/native/py_Dameng.c").read_text(encoding="latin-1") + + connect_keywords = _extract_connect_keywords(conn_c) + conn_methods = _extract_names_from_c_array(conn_c, "g_ConnectionMethods") + conn_members = _extract_names_from_c_array(conn_c, "g_ConnectionMembers") + conn_getset = _extract_names_from_c_array(conn_c, "g_ConnectionCalcMembers") + cur_methods = _extract_names_from_c_array(cur_c, "g_CursorMethods") + cur_members = _extract_names_from_c_array(cur_c, "g_CursorMembers") + cur_getset = _extract_names_from_c_array(cur_c, "g_CursorCalcMembers") + mod_methods = _extract_names_from_c_array(mod_c, "g_ModuleMethods") + exceptions = re.findall(r'SetException\(module,\s*&g_\w+,\s*"([^"]+)"', mod_c) + + conn_getset = [n for n in conn_getset if not n.startswith("DSQL_ATTR_")] + + checks = { + "connect keywords": connect_keywords, + "Connection methods": conn_methods, + "Connection members": conn_members, + "Connection get/set attrs": conn_getset, + "Cursor methods": cur_methods, + "Cursor members": cur_members, + "Cursor get/set attrs": cur_getset, + "module methods": mod_methods, + "exceptions": exceptions, + } + errors: list[str] = [] + for section, tokens in checks.items(): + missing = _missing_tokens(api_doc, tokens) + if missing: + errors.append(f"{section}: {missing}") + if errors: + _fail("API docs coverage missing entries:\n" + "\n".join(errors)) + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--strict-runtime", action="store_true", help="Fail when runtime dmPython check is unavailable") @@ -88,6 +176,8 @@ def main() -> int: _fail(f"Header version mismatch: {header_version} != {version}") _check_docs_version(version) + _check_docs_structure() + _check_api_docs_coverage() _check_smoke_script_dynamic() _check_runtime_version(version, strict=args.strict_runtime)