Skip to content

test: pytest 集成测试套件 + BEGIN/CREATE 类语句的 CommandComplete 修复 - #6

Merged
fanvanzh merged 2 commits into
mainfrom
cursor/pg-protocol-tests-ef99
Apr 21, 2026
Merged

fanvanzh merged 2 commits into
mainfrom
cursor/pg-protocol-tests-ef99

Conversation

@fanvanzh

Copy link
Copy Markdown
Owner

承接已合入的 PR #4,这里补上两件事:一套基于 pytest + psycopg2 的端到端测试,以及一个在写测试时暴露出来的协议层小 bug 的修复。

1. fix(protocol): BEGIN / CREATE / SET 这类语句必须回 CommandComplete,不能回 Rows

DuckDB 对 BEGIN / CREATE / DROP / SET 等语句会返回一个 1 列、名为 Success 的结果。之前代码里 is_select = has_columns && !is_dml 的判断把它们当 SELECT 处理,于是发了 RowDescription + DataRow 给客户端。libpq 收到后把整个结果标成 PGRES_TUPLES_OK("这条语句产出了元组"),这会让 psycopg2 在 autocommit=False 时报:

psycopg2.DatabaseError: error with status PGRES_TUPLES_OK and no message from the libpq

因为 psycopg2 在 autocommit 关闭时会在用户第一条语句之前偷偷发一个 BEGIN;,此时 server 的"假结果"直接把客户端的状态机带坏了。

改法很窄:只对 SELECT / EXPLAIN / EXECUTE / CALL 这几类语句真的回发 rows,其它统一只发 CommandComplete。顺手给 statement_tag_for 补了 PREPARE 和 EXECUTE 两种 tag(EXECUTE 按它承载的 SELECT 计数,返回 SELECT n)。

2. test/: pytest + psycopg2 集成测试

按要求在仓库根建了 test/ 目录,里面的 fixture 会:

  1. 先在 $POSTDUCK_BIN → <repo>/build/postduck → $PATH 里找可执行文件;
  2. 申请一个随机空闲端口 + 临时 data 目录,启动一个真的 postduck 进程;
  3. 等端口 listen 到、再把一条 psycopg2 连接交给每个测试用例;
  4. 全部跑完后 SIGINT 关 server、清理 tmpdir。
  5. 如果设置了 POSTDUCK_URL,就跳过启动,直接连已有实例——方便对接真实环境做回归。

目录结构:

test/
├── README.md              # 使用说明
├── requirements.txt       # psycopg2-binary, pytest
├── run_tests.sh           # 一键脚本(会确保依赖装好)
├── conftest.py            # 启停 server + 公共 fixture
├── test_basic.py          # Simple Query 协议: SELECT / DDL / DML / 多语句 / 大结果集 / ParameterStatus / BackendKeyData
├── test_extended.py       # Extended Query 协议: 参数化、NULL、executemany、fetchmany、server-side PREPARE/EXECUTE
├── test_types.py          # 类型映射: int2/int4/int8, float/double, bool, text/varchar, date, timestamp, decimal, null, 负数
├── test_transactions.py   # BEGIN/COMMIT/ROLLBACK、autocommit、aborted-txn 自动恢复、SET 未知 GUC no-op
└── test_errors.py         # 语法 / catalog / 类型转换错误走正常 ErrorResponse,且错误后连接可继续复用

运行

pip install -r test/requirements.txt
# 先 build 出 build/postduck,然后:
./test/run_tests.sh -v        # 或 pytest -v test/

结果

============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.0.3
collected 39 items

test/test_basic.py ........                                              [ 23%]
test/test_errors.py .....                                                [ 35%]
test/test_extended.py .........                                          [ 58%]
test/test_transactions.py .....                                          [ 71%]
test/test_types.py ...........                                           [100%]

============================== 39 passed in 0.25s ==============================

冲突说明

原分支 cursor/pg-protocol-compat-ef99 在 PR #4 被 squash-merge 进 main 后,所有 4 个 feat/docs commit 被压成了一个 squash commit(8d4ddda),导致原分支相对 main 的历史对不上。

这里重新 git checkout -b cursor/pg-protocol-tests-ef99 origin/main 之后,把 #4 合入之后新增的两个 commit(fix(protocol): emit CommandComplete... 和 test: add pytest integration suite...)cherry-pick 过来,干净应用、无冲突,只改动 src/session.cpp 与新增的 test/ 目录。

Open in Web Open in Cursor 

cursoragent and others added 2 commits April 20, 2026 11:44
DuckDB surfaces a 1-column 'Success' result for statements like BEGIN,
CREATE, DROP, SET, etc. Previously we treated any statement with
columns (and not an obviously DML one) as a SELECT and forwarded a
RowDescription + DataRow to the client. libpq then tagged the result
as PGRES_TUPLES_OK, which breaks drivers that use it as the signal
between tuple-producing and command-complete statements — notably
psycopg2 when autocommit is off (the implicit 'BEGIN;' sent before
the user query).

Now we forward rows only for SELECT / EXPLAIN / EXECUTE / CALL
statement types; everything else emits a plain CommandComplete.
Add PREPARE / EXECUTE tags to statement_tag_for so psycopg2 can
consume server-side PREPARE ... ; EXECUTE ... sequences correctly.

Co-authored-by: fanvanzh <fanvanzh@users.noreply.github.com>
The test/ directory contains a self-contained integration suite:

- conftest.py spins up a real postduck binary on a random port with a
  temp data directory (or reuses a server pointed to by POSTDUCK_URL),
  and hands out psycopg2 connections/cursors to the tests.
- test_basic.py — simple query protocol, DDL/DML, multi-statement.
- test_extended.py — extended query protocol: parameter binding,
  NULL handling, executemany, fetchmany, server-side PREPARE/EXECUTE.
- test_types.py — PG-OID round trips for int/float/bool/text/date/
  timestamp/decimal/null/negative numbers.
- test_transactions.py — autocommit, commit/rollback, aborted-txn
  recovery, SET of unknown GUCs as no-ops.
- test_errors.py — syntax / catalog / conversion errors surface as
  proper ErrorResponse and don't poison the connection.

Run with 'pytest test/' or 'test/run_tests.sh'. 39 tests, all pass
against the current build.

Co-authored-by: fanvanzh <fanvanzh@users.noreply.github.com>
@fanvanzh
fanvanzh marked this pull request as ready for review April 21, 2026 05:52
@fanvanzh
fanvanzh merged commit d88f9ff into main Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants