개발 가이드 (한국어)¶
🌐 DEVELOPMENT.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.
pycubrid를 설정·테스트·기여하는 데 필요한 모든 것.
목차¶
- 사전 준비
- 설치
- 프로젝트 구조
- 테스트 실행
- 오프라인 테스트
- 동기/비동기 재생 패리티
- 통합 테스트
- 코드 커버리지
- Docker 설정
- 코드 스타일
- Makefile 명령
- CI/CD
- 문서 구조
- 아키텍처 개요
- 새 패킷 타입 추가
- 새 데이터 타입 추가
- 릴리스 절차
사전 준비¶
| 요구사항 | 버전 | 비고 |
|---|---|---|
| Python | 3.11+ | pyproject.toml의 requires-python과 동일 |
| Docker | 최신 | 통합 테스트에만 필요 |
| CUBRID Server | 10.2–11.4 | Docker 또는 로컬 설치 |
설치¶
# 저장소 클론
git clone https://github.com/cubrid-lab/pycubrid.git
cd pycubrid
# dev 의존성 그룹과 함께 개발 모드로 설치(pip 25.1 이상)
pip install -e . --group dev
# 또는 Makefile 사용
make install
dev 의존성¶
| 패키지 | 용도 |
|---|---|
pytest |
테스트 프레임워크 |
pytest-cov |
커버리지 리포트 |
ruff |
린터와 포매터 |
프로젝트 구조¶
graph TD
root[pycubrid/]
pkg["pycubrid/ - Main package"]
tests[tests/ - Test suite]
docs[docs/ - Documentation]
pyproject[pyproject.toml - Package configuration]
makefile[Makefile - Development commands]
compose[docker-compose.yml - CUBRID container setup]
changelog[CHANGELOG.md - Release history]
contributing[CONTRIBUTING.md - Contribution guidelines]
license[LICENSE - MIT license]
readme[README.md - Project overview]
root --> pkg
root --> tests
root --> docs
root --> pyproject
root --> makefile
root --> compose
root --> changelog
root --> contributing
root --> license
root --> readme
pkg --> init[__init__.py - Public API, PEP 249 module attributes]
pkg --> connection["connection.py - Sync Connection class"]
pkg --> cursor["cursor.py - Sync Cursor class"]
pkg --> aio["aio/ - asyncio Connection and Cursor"]
pkg --> compat["compat/ - opt-in CUBRIDdb and native compatibility layer"]
pkg --> conncommon[_connection_common.py - Logic shared by sync and async connections]
pkg --> cursorcommon[_cursor_common.py - Logic shared by sync and async cursors]
pkg --> prepared[_prepared_binding.py - Private typed bind encoders]
pkg --> types[types.py - PEP 249 type objects and constructors]
pkg --> exceptions[exceptions.py - PEP 249 exception hierarchy]
pkg --> errcodes[error_codes.py - CAS and CUBRID error codes]
pkg --> constants["constants.py - CAS protocol enums"]
pkg --> protocol["protocol.py - CAS packet classes"]
pkg --> packet[packet.py - PacketWriter + PacketReader primitives]
pkg --> lob["lob.py - LOB (BLOB/CLOB) support"]
pkg --> timing[timing.py - Optional timing statistics]
pkg --> typed[py.typed - PEP 561 marker]
tests --> conftest["conftest.py - Shared fixtures"]
tests --> unit["test_*.py - Behavior-oriented offline test modules"]
tests --> test_integration["Integration modules - Live DB tests (marked integration, require a live CUBRID server)"]
tests --> helpers["helpers/ and fixtures/ - Shared test support and golden data"]
docs --> doc_index[index.md - Documentation index]
docs --> doc_arch[ARCHITECTURE.md - Request flow and module boundaries]
docs --> doc_dev[DEVELOPMENT.md - This file]
docs --> doc_pages["Other guides - see the documentation index"]
이 다이어그램은 설계 문서가 아니라 파일 지도이며, 자주 바뀌는 개수는 의도적으로 싣지 않았습니다. 모듈 경계는 ARCHITECTURE.md, pytest 마커는 pyproject.toml의 markers 목록, 전체 가이드는 문서 색인을 참고하세요.
테스트 실행¶
오프라인 테스트¶
대부분의 테스트는 오프라인입니다 — CUBRID 연결을 목킹하고 데이터베이스 없이 패킷 직렬화, 커서 로직, 타입 매핑, 예외 처리를 테스트합니다.
# 커버리지와 함께 모든 오프라인 테스트 실행
pytest tests/ -v -m "not integration" \
--cov=pycubrid --cov-report=term-missing --cov-fail-under=95
# 또는 Makefile 사용
make test
백슬래시 이스케이프 모드 고정¶
tests/conftest.py는 모든 테스트에서 no_backslash_escapes를 CUBRID 서버 기본값인
True로 자동(autouse) 고정합니다. 대부분의 테스트가 스크립트된 가짜 소켓 위에
Connection/AsyncConnection을 만들고, 그 소켓은 실제 CHAR_LENGTH 이스케이프
프로브에 응답할 수 없기 때문입니다. 실제 프로브가 필요한 모듈(라이브 서버를
쓰거나, 프로브에 응답하는 스크립트된 가짜 브로커를 쓰는 경우)은 모듈의 다른
마커와 함께 pytest.mark.no_escape_pin을 등록해 고정에서 빠집니다. 예:
예전에는 파일 이름 조각을 하드코딩한 목록으로 제외했는데, 관계없는 모듈까지
조용히 일치했습니다(#524, 예: "test_integration"이 모든
test_integration_*.py 파일과 일치). 새 파일 이름 조각을 추가하지 말고 모듈에
마커를 명시하세요.
빠른 드라이버 테스트 vs. 저장소 도구 점검¶
오프라인 테스트 중 repo_tooling 마커가 붙은 하위 집합(pyproject.toml에
등록됨)은 저장소 정책과 도구를 점검합니다 — docs-sync 스크립트, PR 제목
검증기, 릴리스 스크립트, 워크플로 YAML 계약, 공유 품질 게이트 등입니다(#558).
이들은 pycubrid 드라이버 동작을 전혀 포함하지 않고 pycubrid 자체의
커버리지에서도 제외되므로, offline-tests 매트릭스 대신 전용
repo-tooling-tests CI job에서 실행되어 일상적인 드라이버 피드백 속도를
유지합니다. 도구 관련 경로가 변경되면 Linux 단일 레인이 선택됩니다.
CI Gate는 선택된 repo-tooling-tests와 offline-tests의 성공을 요구하며,
의도적으로 선택되지 않은 잡의 스킵만 허용합니다.
# 빠른 드라이버 레인 — 목킹된 드라이버 동작만 (offline-tests가 실행하는 것)
pytest tests/ -m "not integration and not repo_tooling" -v
# 저장소 도구 레인 — 정책/도구 점검 (repo-tooling-tests가 실행하는 것)
pytest tests/ -m "repo_tooling" -v
# 두 레인을 모두, 여전히 오프라인으로 (실제 CUBRID 서버 없이)
pytest tests/ -m "not integration" -v
모듈은 파일 이동이나 경로 기반 수집 규칙이 아니라 명시적인
pytestmark = pytest.mark.repo_tooling으로 도구 레인에 포함됩니다. 따라서
디스크에서 재구성할 필요가 없고 pytest tests/에서 조용히 빠지는 테스트도
없습니다(모든 마커는 기본 수집에 추가적일 뿐이며, 실행 시 선택하거나
제외하는 것은 -m뿐입니다). docs-sync.yml은 의존성을 설치하지 않고
test_docs_reason.py를 순수 python -m unittest discover로 실행하므로, 이
모듈은(같은 위험이 있는 test_pr_title.py도) pytest를
try/except ModuleNotFoundError로 가져오고, 없으면 빈 pytestmark로
대체합니다 — 그곳에서는 마커 자체가 의미가 없기 때문입니다. pytest로만
실행되는 모듈에는 이 보호 장치가 필요 없습니다.
순수 스칼라 포맷 사례는 연결 없이 두 백슬래시 모드를 검사하는
tests/test_param_security.py의 공유 골든 매트릭스에 모읍니다(#563).
tests/test_aio_cursor_parity.py는 동기/비동기 포맷·바인딩 어댑터와 escape
모드 협상 전 거부를 작은 검사로 유지합니다. 일반 복구 동작은 바인딩된 SQL과
재생 세션으로 관찰하고, 세대 경계·손상된 응답 등 외부에서 관찰하기 어려운
안전성 불변식은 화이트박스 테스트로 유지합니다.
동기/비동기 재생 패리티¶
tests/test_replay_parity.py는 동기 Connection과 비동기 AsyncConnection이
같은 브로커에 대해 같게 동작하는지를 오프라인으로 몇 초 안에 검사합니다.
tests/helpers/replay_broker.py는 스레드 기반의 프로세스 내 CAS 브로커입니다.
TCP 세션을 여러 번 받을 수 있어 재접속이 실제로 일어나고, 각 요청에 시나리오별
스크립트로 응답하며(지정하지 않은 요청에는 tests/helpers/cas_reply.py로 만든
결정적인 기본 응답), 받은 모든 요청을 기록합니다.
시나리오는 공개 연산(open, close, connect, execute, fetchall,
commit, ping 등)의 목록과 스크립트로 이루어집니다. 두 드라이버에서 각각 새
브로커로 재생한 뒤 네 가지를 비교합니다: 단계별 결과(반환값 또는 예외 타입),
보낸 요청 전체(세션, CAS 함수, 되돌려 보낸 CAS_INFO, 인자), 이후 연결을 계속 쓸 수
있는지(ping(reconnect=False)), TCP 세션 수. 시나리오는 드라이버가 의도적으로
다른 항목을 이유와 함께 적고, 그 밖의 차이는 모두 실패입니다. 아직 고치지 않은
알려진 차이는 unintended=로 기록해 strict xfail로 실행합니다. 각 시나리오의
check는 두 관측 결과 모두에 실행되므로 시나리오가 원래 경로를 조용히 벗어날 수
없습니다.
다루는 범위: 연결/종료, 종료 후 재접속, autocommit 설정/복원(명시적 설정, 생성자,
미설정), commit/rollback에 의한 핸들 무효화, OUT_TRAN CHECK_CAS 검사, 한 번의
CHECK_CAS 복구(autocommit 복원과 이스케이프 모드 재감지 포함)와 복구 실패, 교체된
세션에 바인딩된 SQL(세대 펜스, #471/#485), 복구 여부별 ping() 실패, 잘못된
응답과 잘린 응답(#533), 세션을 유지하는 DataError(#512), fetch 페이지
DataError 계약(#536). 태스크 취소는 pycubrid.aio에만 있으므로 대신
tests/test_async_cancellation.py에서 다룹니다.
작업별 라운드트립 예산(#557): Observation.step_functions(i)는
steps[i]만 실행하는 동안 보낸 정확하고 순서가 있는 CAS 함수 목록을
반환합니다 — connect/setup(생성자 autocommit 세터와 백슬래시 이스케이프 모드
프로브를 포함하는 0번 단계)과 다른 모든 단계로부터 분리됩니다.
FIRST_INSERT_BUDGET, REUSED_CURSOR_INSERT_BUDGET,
SELECT_TO_INSERT_BUDGET, MANUAL_INSERT_EXECUTE_BUDGET /
MANUAL_INSERT_COMMIT_BUDGET, FETCH_PAGINATION_BUDGET,
ESCAPE_EXPLICIT_* / ESCAPE_AUTOMATIC_* 예산이 이 정확한 시퀀스에 이름을
붙입니다. 각각 리스트 동등성으로 검사하므로, 누락된 안전 요청(예: 빠진
CHECK_CAS 생존 확인)과 추가된 라운드트립을 똑같이 잡아냅니다 — 어느 쪽도
"최적화"로 통과할 수 없습니다. 각 예산은 작업 성공과 연결 재사용 가능 여부도
확인하며, fetch 시나리오는 반환 행을 검사합니다. 따라서 요청 수가 같더라도
잘못된 응답이나 결과로 성공할 수 없습니다. 이들의 스크립트(_autocommit_insert,
_manual_insert_last_insert_id)는 INSERT의 PREPARE_AND_EXECUTE에
명시적인 OUT_TRAN(autocommit: 암묵적 트랜잭션이 이미 커밋됨) 또는
IN_TRAN(수동: commit()을 위해 열어둠) 상태로 응답하고,
GET_LAST_INSERT_ID에 올바른 형식의 값을 줍니다 — 브로커의 일반 기본
응답(단순 응답 코드)은 드라이버가 (정확하게) 손상된 응답으로 거부합니다.
이 시나리오들은 향후 라운드트립 축소 작업(#419/#488/#525)의 재현 기준선이며,
프로덕션 최적화와 CHECK_CAS 제거는 이 작업의 범위 밖입니다.
위 예산은 statement pooling이 꺼져 있다고 알리는 브로커에서 실행되며, 지연 닫기(#488)는 그때 적용되지 않으므로 바뀌지 않습니다. Scenario.statement_pooling=1이면 브로커가 pooling을 켜짐으로 알립니다. REUSED_CURSOR_INSERT_POOLED_BUDGET(이전 INSERT의 CLOSE_REQ_HANDLE과 그 앞의 CHECK_CAS가 빠져 6개 대신 4개 요청)과 SELECT_TO_INSERT_POOLED_BUDGET(CLOSE_REQ_HANDLE이 빠져 4개 대신 3개)은 지연 닫기가 없애는 요청을 고정하며, 그 검사는 다음 PREPARE_AND_EXECUTE가 해제된 핸들 id를 정확히 싣는지 확인합니다.
tests/test_deferred_close.py의 추가 시나리오는 대기열 초과, 트랜잭션 경계의
전체 핸들 정리와 재접속 안전성을 검사합니다.
시나리오를 추가하려면 SCENARIOS에 단계, _on(...)으로 만든 스크립트(예: 응답 후
CAS를 재활용하는 _hang_up_after_ok), check를 갖춘 Scenario를 추가합니다.
의도된 차이 (실패 아님):
| 차이 | 이유 |
|---|---|
AsyncConnection(...)은 연결하지 않고 await pycubrid.aio.connect(...)가 연결 |
__init__은 await할 수 없음 |
비동기 autocommit은 await conn.set_autocommit(v)로 설정 |
프로퍼티 setter는 await할 수 없음 |
| 비동기 I/O 메서드는 모두 코루틴 | asyncio API |
비동기 create_lob()은 NotSupportedError를 발생시키고 LOB_NEW를 보내지 않음 |
비동기 LOB 미지원(시나리오 create_lob) |
| 태스크 취소 | 비동기 전용, 패리티에서 제외 |
하니스가 찾은 의도치 않은 차이 (모두 #521에서 수정):
| 차이 | 수정 |
|---|---|
동기 close() 후 connect()가 명시적 autocommit을 다시 보내지 않음(#520) |
connect()가 모든 대체 세션에서 명시적 세션 상태를 복원 |
동기 connect(autocommit=True)가 재접속하는 프로퍼티 setter를 사용: SET_DB_PARAMETER와 COMMIT 사이에 CHECK_CAS가 추가되고, 그 사이 CAS가 재활용되면 두 요청이 다른 세션으로 나뉘며, 실패 시 원래 오류가 나오고 소켓이 열린 채 남음 |
비동기처럼 연 세션에서만 적용하고 실패 시 OperationalError |
동기 ping(reconnect=False)가 CHECK_CAS 음수 응답 후에도 세션을 유지해 다음 요청이 조용히 재접속함 |
비동기처럼 손상된 세션을 닫음 |
CHECK_CAS 대체 세션에서 비동기 이스케이프 감지가 auto_commit=0을 보냄. 다른 모든 이스케이프 감지(두 드라이버)는 연결의 값을 보냄 |
대체 세션의 감지도 연결의 값을 보냄 |
같은 시점에 공통 결함(패리티 차이 아님)도 수정했습니다: 자동 이스케이프 감지에서
감지 ROLLBACK 직후 CAS가 재활용되면 autocommit 적용 전에 connect()가
실패했습니다. 이제 두 드라이버 모두 그 OUT_TRAN 세션을 먼저 CHECK_CAS로
확인하고 한 번 교체합니다.
또 다른 공통 결함도 #551에서 수정했습니다(시나리오
autocommit_setter_survives_recycle_after_set_db_parameter): 공개 autocommit
setter는 두 요청 사이에 CAS가 재활용되면 SET_DB_PARAMETER와 COMMIT을 서로 다른
CAS 세션으로 보냈습니다. 이제 COMMIT 전에 새 값을 기록하므로 그 요청의 한 번뿐인
재접속이 대체 세션에 새 값을 먼저 복원합니다. COMMIT이 실패하면 연결을 닫고 이전
값을 유지합니다.
뮤테이션 테스트¶
라인 커버리지는 코드가 실행된다는 것을 보여 주고, 뮤테이션 테스트는 테스트가
잘못된 동작을 잡아낸다는 것을 보여 줍니다. 드라이버 코어(패킷/프로토콜
직렬화, 커서/연결 수명 주기, LOB I/O)는 pyproject.toml의 [tool.mutmut]에
설정되어 있으며 오프라인 스위트를 대상으로 실행합니다:
make mutation은 Python 3.12 이상이 필요하며 3.11에서는 메시지와 함께 멈춥니다.
mutmut 3.8이 3.11에서 통계 수집 단계에 실패하기 때문입니다(#812). CI의 주간
뮤테이션 작업은 3.12를 사용합니다.
단순 점수가 아니라 의미 있는 생존 변이(어떤 테스트도 잡지 못한 뒤집힌 비교나 빠진 정리 코드)에 집중하세요.
통합 테스트¶
통합 테스트(integration 마커)는 실행 중인 CUBRID 인스턴스가 필요합니다.
가장 간단한 방법은 Makefile로 Docker를 사용하는 것입니다:
make integration # 브로커를 localhost:33000에 게시
make integration CUBRID_TEST_PORT=33522 # 다른 컨테이너가 쓰지 않는 포트 사용
make integration은 scripts/run_integration.py를 실행하며, 이 스크립트는 compose 서비스를 시작하고, 고정 시간 대기 대신
scripts/wait_for_cubrid.py로 준비 상태를 기다리며(약 3분 안에 준비되지 않으면
실행 실패), 모든 엔드포인트 필드를 명시적으로 설정해
-m "integration and not tls"를 실행합니다. 이어서
scripts/check_integration_lanes.py --results로 JUnit 보고서를 검사해 전부
건너뛰었거나 분류되지 않은 skip이 있으면 실패시키고, 생성한 리소스는 항상 제거합니다.
종료 상태는 처음 실패한 단계의 상태이며, docker compose down -v가 실패하면 이를 보고하고
그 외에는 통과한 실행도 실패로 처리합니다.
TLS 테스트는 SSL이 켜진 브로커가 필요합니다.
비동기 TLS 통합 테스트를 참고하세요.
결과 파일(#1024). make test는 test-results/test/에 junit.xml, coverage.xml,
summary.json, environment.json을 쓰고, make integration과 make integration-tls는
test-results/integration/, test-results/integration-tls/에
junit.xml, summary.json, environment.json(실패한 실행에서는 service.log도)을 씁니다.
이 디렉터리는 gitignore 대상이며 make clean이 삭제합니다.
CI도 오프라인, 통합, TLS 통합 job에서 같은 구조로 결과를 쓰고, 한 줄 요약을 job 요약에
추가하며, 성공·실패와 관계없이 매 실행마다 test-results/를
test-results-<lane>-py<python>[-cubrid<version>]라는 7일 보관 아티팩트로 업로드합니다.
scripts/report_results.py가 summary.json(schema_version, lane, status, exit_code,
결과별 counts, coverage)을 만들고 같은 상태와 개수를 콘솔 한 줄로 출력합니다.
status는 통과한 테스트가 하나 이상이고 실패·오류가 없으며 종료 상태가 0일 때만 PASS,
테스트가 실패했거나 명령이 0이 아닌 상태로 끝나면(커버리지 하한 미달 포함) FAIL,
실행되었다는 증거가 없으면(JUnit 파일 없음, 수집된 테스트 없음, 전부 skip) INCOMPLETE입니다.
실패한 뒤 teardown에서도 오류가 난 테스트는 가장 나쁜 결과로 한 번만 셉니다.
environment.json은 Python, pycubrid, CUBRID 이미지 버전, 플랫폼, 명령만으로 만들어지고
프로세스 환경 변수는 읽지 않으므로, export한 CUBRID_PASSWORD는 결과 파일에 들어가지 않습니다.
명령을 저장하기 전에 인증 정보가 포함된 URL의 비밀번호(cubrid://dba:***@host)와
password, passwd, pwd, token, secret, api_key 키의 값(--password=***,
--password ***, CUBRID_PASSWORD=***)을 가립니다. 이는 패턴 기반의 최선 노력 처리이며
비밀 탐지기가 아니므로, 다른 형태의 인증 정보는 그대로 저장됩니다. 인증 정보는 명령줄이 아니라
환경 변수로 전달하십시오. 이 파일들은 명령의 종료 상태를 바꾸지 않습니다. 통합 테스트 레인의 summary.json은 정리 뒤에 쓰이므로 최종 종료 상태를 담고(down -v 실패는 FAIL), service.log 저장은 실패해도 경고만 남기고 정리를 건너뛰지 않습니다. 프로젝트 잠금을 잡은 실행이 결과 파일을 소유하며 이전 실행의 파일을 먼저 지웁니다. 잠금 이후의 거부(이미 있는 프로젝트, 소유권 확인 실패)나 잘못된 INTEGRATION_PROJECT는 reason이 있는 FAIL 요약을 남기고, 다른 실행이 잠금을 쥐고 있어 거부된 실행은 아무것도 쓰거나 지우지 않습니다.
INTEGRATION_RESULTS=<경로>로 통합 테스트 JUnit 파일 위치를 바꾸면 나머지 파일도 그 옆에 쓰입니다.
CI 작업은 이 타깃을 실행하지 않습니다. CI 라이브 작업과 Make 계약을 참고하세요.
검증 요약(make verify, #1053). make verify는 scripts/run_verify.py를 exec하며, 이
스크립트가 make check, make test, make integration을 차례로 실행하고 처음 실패한 단계에서
멈춥니다. 실패나 시그널 뒤에도 항상 test-results/verify/를 씁니다(VERIFY_RESULTS=<디렉터리>로
위치를 바꿀 수 있습니다). 먼저 그 디렉터리에 있던 이전 실행의 파일을 지우며, make -n verify는
아무것도 쓰지 않습니다.
| 파일 | 내용 |
|---|---|
summary.json |
schema_version, lane(verify), 전체 status와 problems, 그리고 단계 순서(check, test, integration)대로 단계마다 하나씩 있는 phases 항목(status, exit_code, 읽은 레인 summary와 그 요약의 summary_status) |
junit/test.xml, junit/integration.xml |
실행되어 JUnit 파일을 쓴 레인의 JUnit 사본 |
environment.json |
live 레인이 실행되어 쓴 environment.json의 사본 |
단계는 종료 상태가 0이고 자신의 summary.json(test와 integration)을 읽을 수 있으며 그 값이
PASS일 때만 PASS입니다. 앞 단계가 실패해 시작하지 않은 단계는 NOT_RUN입니다. 전체 상태는
실패한 단계가 있으면 FAIL, 실행되지 않은 단계가 있거나 레인 요약이 없거나 읽을 수 없거나
PASS/FAIL/INCOMPLETE 문자열 상태가 아니거나, 레인이 아무것도 증명하지 못했거나 결과 파일을
복사하지 못하면 INCOMPLETE, 모든 단계가 통과했을 때만 PASS입니다. 마지막 콘솔 줄이 이를
반복합니다(예: verify: PASS check=PASS test=PASS integration=PASS ->
test-results/verify/summary.json). make verify는 처음 실패한 단계의 종료 상태를 유지하고,
시그널 N 뒤에는 128 + N으로, 요약이 PASS가 아니면 1로 종료합니다. 실패한 오프라인 테스트가
JUnit 보고서에 비밀번호를 출력할 수 있으므로, 모든 사본에서 CUBRID_PASSWORD,
CUBRID_TEST_PASSWORD, CUBRID_TLS_TEST_PASSWORD의 값과 CUBRID_TEST_URL의 비밀번호(퍼센트
인코딩, XML·JSON 이스케이프 형태 포함)를 파싱한 텍스트·속성·문자열 값에서 ***로 바꾸므로 사본은
유효한 XML과 JSON으로 남습니다(파싱할 수 없는 파일은 바이트 단위로 가립니다). 요약에는 명령, URL, 환경 변수를
기록하지 않습니다.
단계는 호출한 프로세스 그룹에 남으므로 Ctrl-C와 터미널 hangup은 평소처럼 단계에 전달됩니다.
GNU make는 레시피에 SIGTERM만 전달합니다(timeout make verify, kill <make pid>). 러너는 처음
받은 SIGINT, SIGTERM, SIGHUP을 실행 중인 단계의 make에 SIGTERM으로 전달하고, make integration은
자신의 러너를 exec하므로 live 레인은 여전히 Compose 프로젝트를 제거합니다. 이후 러너는 다음
단계를 시작하지 않고 요약을 씁니다. 터미널 밖에서 make에만 보낸 SIGINT나 SIGHUP은 러너에
전달되지 않습니다.
make docker-up과의 격리(#1021). make integration과 make integration-tls는 자체 Compose 프로젝트(기본값 pycubrid-it-<타임스탬프>-<pid>)에서 실행되며 모든 docker compose 호출에 -p <project>를 붙이므로, make docker-up으로 띄운 CUBRID에 붙거나 재시작하거나 제거하지 않습니다(내보낸 COMPOSE_PROJECT_NAME도 무시됩니다). 이름은 INTEGRATION_PROJECT=<이름>(^[a-z0-9][a-z0-9_-]*$)으로 정할 수 있으며, 실행 전에 해당 프로젝트에 컨테이너·볼륨·네트워크가 없는지 확인하고 있으면 아무것도 시작하거나 제거하지 않은 채 "Refusing to run"과 함께 종료 코드 1로 끝납니다. 같은 이름의 실행이 진행 중일 때 두 번째 실행도 거부되며("another run owns project"), 정리가 끝날 때까지 임시 디렉터리의 잠금 파일에 건 flock으로 보장합니다(POSIX 전용, Windows에서는 이 보호가 생략됩니다). 프로젝트가 실행 자신의 것이므로 성공·준비 실패·테스트 실패·SIGINT·SIGTERM·SIGHUP 후의 정리는 docker compose -p <project> down -v이며, 이 실행의 볼륨도 삭제하므로 매 실행이 빈 데이터베이스에서 시작하고 아무것도 남기지 않습니다. 수동 랩의 데이터 볼륨은 건드리지 않습니다. make docker-down은 수동 랩의 데이터를 보존하고, make docker-clean(기본 프로젝트에 대한 down -v)만 이를 삭제합니다. 기존 브로커를 유지하며 테스트하려면 Docker를 관리하지 않는 make integration-local을 사용하세요. SIGINT, SIGTERM, SIGHUP에서도 정리가 실행되지만(현재 단계를 멈추고 130으로 종료), 비대화형 셸의 백그라운드 작업(&)으로 시작한 make integration은 POSIX에 따라 SIGINT를 무시하며 SIGTERM에서는 여전히 정리가 실행됩니다.
통합 테스트 활성화와 엔드포인트 선택의 구분. 통합 테스트는
CUBRID_TEST_URL 또는 CUBRID_TEST_HOST가 비어 있지 않은 값으로 설정되면
활성화됩니다. 엔드포인트는 모든 통합 모듈, tests/conftest.py 게이트,
scripts/wait_for_cubrid.py가 함께 쓰는 공유 헬퍼 tests/_cubrid_endpoint.py가
필드별로 결정합니다:
- 필드별 변수
CUBRID_TEST_HOST,CUBRID_TEST_PORT,CUBRID_TEST_DB,CUBRID_TEST_USER,CUBRID_TEST_PASSWORD가 우선합니다. - 없으면
CUBRID_TEST_URL=cubrid://user[:password]@host[:port]/database의 해당 구성 요소를 사용합니다. - 그것도 없으면 기본값
localhost,33000,testdb,dba, 빈 비밀번호를 씁니다.
비어 있는 필드별 변수는 설정되지 않은 것으로 보지만, CUBRID_TEST_PASSWORD=""는
명시적인 빈 비밀번호입니다. 스킴이 없는 CUBRID_TEST_URL(예: 1)은 통합 테스트를
활성화하기만 합니다. 다른 스킴, 호스트 누락, 숫자가 아닌 포트, 잘못된 데이터베이스
이름을 가진 CUBRID_TEST_URL은 모든 통합 테스트를 error로 만듭니다(오프라인
테스트에는 영향 없음).
CI처럼 URL과 필드별 변수를 함께 내보내면 이전과 똑같이 동작하며, 기본값이 아닌
호스트나 포트를 가리키는 URL은 이제 조용히 localhost:33000을 테스트하는 대신
그대로 사용됩니다.
이미 실행 중인 서버(Docker 수명 주기 없음)를 대상으로 할 때는 엔드포인트를 명시적으로, 가급적 필드별 변수로 지정하세요:
CUBRID_TEST_HOST=127.0.0.1 CUBRID_TEST_PORT=33522 \
CUBRID_TEST_DB=testdb CUBRID_TEST_USER=dba CUBRID_TEST_PASSWORD= \
pytest tests/ -m "integration and not slow and not tls" -v
# 동일: CUBRID_TEST_URL="cubrid://dba@127.0.0.1:33522/testdb" pytest ...
skip과 error. 엔드포인트가 설정되지 않으면 통합 테스트는 건너뛰므로 인자 없는
pytest는 계속 통과합니다. 엔드포인트가 설정되어 있으면 게이트가 세션당 한 번
(SELECT 1, 5초 제한 시간) 접속을 확인하고, 연결할 수 없으면 모든 일반 통합
테스트가 건너뛰는 대신 엔드포인트(비밀번호 제외)와 연결 오류를 담아 error로
보고되며 pytest는 0이 아닌 코드로 종료합니다. 어떤 테스트 모듈도 import 시점에
서버에 접속하지 않습니다. 통합 테스트를 건너뛰려면 CUBRID_TEST_URL과
CUBRID_TEST_HOST를 모두 해제하세요.
tests/test_integration_cas_reconnect.py의 CAS 재활용 회귀 테스트(#485)는 서버
컨테이너 안에서 broker_changer로 브로커 파라미터를 바꾸고 cubrid broker reset을
실행합니다. CUBRID_TEST_DOCKER_CONTAINER가 그 컨테이너를 가리키지 않으면 건너뜁니다.
예: export CUBRID_TEST_DOCKER_CONTAINER="$(docker compose ps -q cubrid)".
make integration과 CI 통합 레인은 이 값을 설정하며, 모든 변경은 테스트가 끝나기 전에
원래대로 복원됩니다.
비동기 TLS 통합 테스트¶
tests/test_aio_ssl_integration.py는 pycubrid.aio의 비동기 TLS 커버리지를 추가하고,
tests/test_tls_matrix_integration.py는 TLS 부정 사례 및 수명 주기 매트릭스(알 수 없는 CA, 호스트명 불일치, 양방향의 평문/TLS 거부, 고정된 TLS 1.2/1.3, ssl=True와 호출자 SSLContext의 비교, 읽기 제한 시간 및 전송 연결 끊김 뒤의 TLS 재접속, 파일 디스크립터 누수)를 동기·비동기 드라이버 모두에 대해 실행합니다. 둘 다 integration과 tls 마커를 가집니다.
저장소의 기본 docker-compose.yml은 평문 브로커만 시작하므로, 별도의 TLS 활성 브로커를 가리키지 않는 한 이 테스트들은 건너뜁니다.
매트릭스 중 브로커와 무관한 절반(만료된 인증서와 자가 서명 인증서, 중단되거나 멈춘 핸드셰이크, TLS 버전 하한, 재접속 시 다운그레이드 시도, 평문 폴백 없음)은 tests/test_tls_matrix_offline.py에서 프로세스 내 OpenSSL 피어(tests/helpers/tls_broker.py)를 상대로 오프라인으로 실행되므로 make test에 포함됩니다. 테스트용 PKI는 tests/fixtures/tls/에 있으며 tests/fixtures/tls/generate.sh로 다시 생성합니다. 중단된 핸드셰이크에서 비동기 연결이 멈추는 문제(#513)는 별도의 오프라인 회귀 스위트 tests/test_aio_tls_handshake_hang.py가 다룹니다.
필요에 따라 일반 통합 변수에 이 TLS 오버라이드를 추가로 내보내세요:
export CUBRID_TLS_TEST_HOST=localhost
export CUBRID_TLS_TEST_PORT=33001
export CUBRID_TLS_TEST_DB=testdb
export CUBRID_TLS_TEST_USER=dba
export CUBRID_TLS_TEST_PASSWORD=
# 선택: ssl.SSLContext/load_verify_locations()용 사설 CA 번들.
export CUBRID_TLS_TEST_CA_FILE="$PWD/certs/ca.pem"
# 선택: 호스트명 불일치 커버리지용 대체 도달 가능 호스트/IP.
export CUBRID_TLS_TEST_MISMATCH_HOST=127.0.0.1
# 선택: 같은 서버의 SSL=OFF 브로커 포트(기본 query_editor 브로커는
# 30000에서 수신). TLS 클라이언트의 평문 브로커 접속 거부 커버리지용.
export CUBRID_TLS_TEST_PLAIN_PORT=30000
# 브로커가 사설 CA를 쓰면 프로세스 기본 신뢰 저장소도 그 CA로 지정해
# ssl=True 사례(test_aio_ssl_connect_default_context와 매트릭스의
# ssl=True 행)가 브로커를 검증할 수 있게 함.
export SSL_CERT_FILE="$CUBRID_TLS_TEST_CA_FILE"
선택 변수는 로컬에서 개별 사례만 제어합니다. 사례는 해당 설정이 없을 때만 건너뜁니다. CUBRID_TLS_TEST_CA_FILE이 설정되면, 연결할 수 없거나 TLS를 제공하지 않는 브로커는 테스트를 건너뛰게 하는 대신 실패시킵니다. CI에서는 모든 변수가 설정되며 scripts/check_integration_lanes.py가 이 모듈들에 스킵이 하나라도 있으면 TLS 레인을 실패 처리하므로, 조용히 건너뛰는 레인은 녹색이 아니라 빨간색이 됩니다.
브로커 측 TLS가 이미 활성화되어 있어야 하고(cubrid_broker.conf의 SSL=ON) 브로커 인증서가 CUBRID_TLS_TEST_HOST와 일치해야 합니다. 그런 다음:
CI의 자동화된 TLS 커버리지¶
일상 개발에서 위 단계를 로컬로 실행할 필요는 없습니다 — .github/workflows/integration-full.yml에 다음을 수행하는 integration-tls 잡(Python {3.11, 3.14, 3.15} × CUBRID 11.4)이 포함되어 있습니다:
- CUBRID 11.4 컨테이너를 수동으로 시작 (컨테이너 기동 후 브로커 설정을 패치할 수 있도록).
- 새 자가 서명 인증서를 생성하고(
CN=localhost,SAN=DNS:localhost),cas_ssl_cert.{crt,key}로 컨테이너에 주입한 뒤BROKER1의SSL=OFF→SSL=ON으로 전환하고 브로커를 재시작해 새 인증서가 반영되게 함. - 생성된 CA 번들을
CUBRID_TLS_TEST_CA_FILE과SSL_CERT_FILE로 Python 테스트 픽스처에 내보냄. - 실제 TLS 핸드셰이크로 브로커를 프로브하고, TLS가 실제로 서비스 중이 아니면 잡을 크게 실패시킴 — 조용한 스킵은 명시적으로 거부됨.
CUBRID_TLS_TEST_*환경 변수를 자동 연결해 TLS 브로커에 대해 모든integration and tls테스트(tests/test_aio_ssl_integration.py와tests/test_tls_matrix_integration.py)를 실행. 컨테이너의SSL=OFFquery_editor브로커용CUBRID_TLS_TEST_PLAIN_PORT=30000도 포함.
TLS 레인은 호스트 이름 검증 실패를 포함해 선택된 모든 테스트가 실행되어야 하며, 브로커 설정 누락으로 인한 스킵은 허용하지 않습니다. 브로커 상태 확인 및 재시작은 서비스 소유자
cubrid로 실행해 실제 브로커를 제어합니다.
이 잡은 integration-full의 나머지와 같은 트리거(수동 workflow_dispatch,
그리고 publish-pypi.yml이 호출하는 릴리스 게이트)로 실행됩니다. ci.yml은
PR마다 같은 레인을 Python 3.14 × CUBRID 11.4 단일 셀로 실행하되, TLS 관련
경로(연결 모듈, pycubrid/__init__.py, pycubrid/protocol.py,
pycubrid/aio/, TLS 및 SSL 테스트, tests/helpers/tls_*.py,
tests/fixtures/tls/, 레인 감사 스크립트 또는 워크플로)가 변경될 때만
실행하므로 일상적인 PR은 그 비용을 치르지 않습니다.
코드 커버리지¶
현재 오프라인 검사와 강제되는 95% 커버리지 하한은 make test로
확인합니다. 측정된 커버리지는 각
전체 커버리지 실행의 결과이며, 일반 PR 스모크와 고위험 PR 회귀 검사는
커버리지를 측정하지 않습니다. 실행 선택은 CI 실행 정책을
참고하세요. 테스트 수는 현재 테스트에
따라 바뀌며 문서의 고정 기준값이 아닙니다.
# HTML 커버리지 리포트 생성
pytest tests/ -m "not integration" \
--cov=pycubrid --cov-report=html --cov-fail-under=95
# 브라우저에서 열기
open htmlcov/index.html
Docker 설정¶
docker-compose.yml¶
services:
cubrid:
image: cubrid/cubrid:${CUBRID_VERSION:-11.2}
environment:
CUBRID_DB: testdb
CUBRID_DB_HOST: localhost
ports:
- "127.0.0.1:${CUBRID_TEST_PORT:-33000}:33000"
volumes:
- cubrid-data:/var/lib/cubrid
- cubrid-databases:/home/cubrid/CUBRID/databases
volumes:
cubrid-data:
cubrid-databases:
실제 파일에는 csql -u dba testdb -c "SELECT 1"을 실행하는 healthcheck도 있습니다.
브로커는 127.0.0.1에만 게시되므로 이 호스트에서는 접근할 수 있지만 네트워크에서는 접근할 수 없습니다.
지원하는 재정의 값은 CUBRID_VERSION(기본값 11.2)과 CUBRID_TEST_PORT(기본값 33000)입니다.
서비스에 고정 container_name이 없으므로 별도의 Compose 프로젝트(COMPOSE_PROJECT_NAME 또는
docker compose -p)를 서로 다른 포트에서 동시에 실행할 수 있습니다.
CUBRID_DB_HOST: localhost를 설정하면 이미지가 databases.txt에 testdb를
localhost로 등록합니다. 11.3까지의 이미지는 이 파일과 데이터베이스를 cubrid-data 볼륨에, 11.4는 cubrid-databases에
두므로 지원하는 모든 버전에서 docker compose down이 데이터베이스를 남깁니다. 11.3까지의 버전은
cubrid-data를 함께 쓰고, 한 CUBRID 버전이 만든 데이터베이스가 다른 버전에서 열린다는 보장은 없으므로
CUBRID_VERSION을 바꾸기 전에 make docker-clean(테스트 데이터 삭제)을 실행하세요. 이 값이 없으면 데이터베이스를 만든 컨테이너의 ID가 기록되고,
그 컨테이너를 다시 만들면 데이터베이스를 사용할 수 없게 됩니다
(문제 해결).
명령¶
Makefile이 수동 수명 주기를 감싸며, 각 타깃은 CUBRID_TEST_PORT를 Compose에 전달합니다:
make docker-up # CUBRID 시작 (127.0.0.1:33000)
make docker-up CUBRID_TEST_PORT=33522 # 다른 호스트 포트에서 시작
make docker-down # 컨테이너와 네트워크 제거, 데이터 유지
make docker-clean # 파괴적: 데이터 볼륨까지 삭제
# 기본 CUBRID 11.2로 시작
docker compose up -d
# 특정 버전으로 시작
CUBRID_VERSION=11.4 docker compose up -d
# 컨테이너 상태 확인
docker compose ps
# 로그 보기
docker compose logs -f cubrid
# 중지 (데이터 볼륨 유지)
docker compose down
# 중지 및 데이터 볼륨 삭제 (테스트 데이터가 모두 삭제됨)
docker compose down -v
# CUBRID_DB_HOST 설정 전에 만든 볼륨 복구 (데이터 유지)
make docker-fix-db-host
연결 상세¶
| 파라미터 | 값 |
|---|---|
| Host | localhost |
| Port | 33000 |
| Database | testdb |
| User | dba |
| Password | (비어 있음) |
코드 스타일¶
Ruff 설정¶
관례¶
- 임포트: 모든 모듈에
from __future__ import annotations - 타입 힌트: 완전한 타이핑. PEP 561 준수 (
py.typed) - super(): 항상
super().__init__(), 절대super(ClassName, self)아님 - 행 길이: 100자
- 독스트링: 모든 공개 메서드와 클래스에 Google 스타일
- 네이밍:
- 클래스:
PascalCase(예:PacketWriter,ColumnMetaData) - private 메서드:
_underscore_prefix(예:_parse_byte,_write_int) - 상수:
UPPER_SNAKE_CASE(예:CAS_INFO,DATA_LENGTH)
린팅¶
# 문제 확인 (두 명령 모두 파일을 수정하지 않음)
make lint # ruff check
make format-check # ruff format --check
# 자동 수정 (파일을 수정하는 유일한 품질 대상)
make format
# 도구 버전과 현재 환경을 별도로 검사
make tooling-check
Make와 일반/정기 CI는 Makefile의 LINT_PATHS 목록을 공유합니다:
pycubrid tests scripts demos examples agent-evals. Ruff CLI와 훅은 명시적으로 Python/pyi만 검사하므로
Markdown은 이 포맷 범위에 포함하지 않습니다. 훅에도 같은 관리 파일 범위를 적용하며,
Mypy는 패키지에 대한 엄격한 검사를 유지합니다. make typecheck와 CI는
tests/typing_self_contract.py 정적 계약도 검사하며, pre-commit 훅은 pycubrid/만 검사합니다. 검사는 현재 Python 환경의
도구를 사용하므로 dev 의존성 그룹을 설치하고 해당 환경을 활성화하세요.
Ruff/Mypy의 정확한 버전은 pyproject.toml의 lint 의존성 그룹에서 관리합니다. Ruff와 Mypy
pre-commit 훅은 repo: local / language: system 훅으로, 같은 활성 환경에서
python3 -m ruff와 python3 -m mypy를 직접 호출합니다. 따라서 별도로 맞춰야 할
훅 리비전이 없습니다: lint 그룹 핀을 올리고(Dependabot의 pip 생태계가 정확히 이
작업을 수행합니다) dev 그룹을 다시 설치하면 충분합니다. 커밋 시 훅이 실행되길
원한다면 그 환경(또는 이를 설치한 venv)을 항상 활성화해두세요. 그렇지 않으면
Ruff/Mypy가 없거나, 고정된 버전 대신 오래되거나 전역에 설치된 버전이 조용히
실행됩니다. lint 그룹 핀을 갱신하고 dev 그룹을 다시 설치한 뒤 make check 및
pre-commit run --all-files를 실행하세요. make tooling-check는 린트/포맷/타입
검사 전에 핀, 설치 버전, 훅 범위 및 CI 범위의 불일치를 실패 처리합니다.
안티패턴 (절대 금지)¶
- SQL 쿼리에 f-string 보간 금지 (SQL 인젝션 위험)
super(ClassName, self)금지 —super()만 사용- Python 2 구조 금지
- 빈
except블록 금지 (close()같은 정리 경로는 예외) - 타입 억제 금지 (설명 없는
# type: ignore)
Makefile 명령¶
| 명령 | 설명 |
|---|---|
make help |
목적별로 묶은 대상 목록 표시 |
make test |
커버리지와 함께 오프라인 테스트 실행; test-results/test/(JUnit, 커버리지, summary.json) 작성 |
make verify |
전체 검증: make check, make test, 이어서 live make integration 레인; Docker가 필요하고, 레인을 실행할 수 없거나 모든 live 테스트가 skip되면 실패하며, 항상 test-results/verify/를 씀 |
make install |
모든 의존성과 함께 개발 모드 설치 |
make doctor |
필수 도구 확인; 하나라도 없거나 동작하지 않으면 실패 |
make lint |
ruff check 실행 (파일 수정 없음) |
make format-check |
ruff format --check 실행 (파일 수정 없음) |
make format |
린트와 포맷 문제 자동 수정 (파일을 수정하는 유일한 품질 대상) |
make check |
정적 품질 게이트: lint, format-check, typecheck, security, docs-reason-check (make check-all은 폐기 예정 별칭) |
make integration |
Docker(실행 전용 Compose 프로젝트) → 준비 대기 → 통합 테스트 → skip 검사 → 정리 (CUBRID_TEST_PORT=<port>로 브로커 포트 변경, INTEGRATION_PROJECT=<name>으로 프로젝트 이름 지정) |
make integration-local |
이미 실행 중인 서버에 대한 통합 테스트 (CUBRID_TEST_URL 또는 CUBRID_TEST_HOST/PORT) |
make clean |
빌드 산출물 제거 |
CI/CD¶
일반 및 전체 통합 워크플로는 테스트 전에
python scripts/wait_for_cubrid.py를 실행합니다. 이 스크립트는 테스트
스위트와 똑같이 엔드포인트를 결정합니다(필드별 CUBRID_TEST_HOST,
CUBRID_TEST_PORT, CUBRID_TEST_DB, CUBRID_TEST_USER,
CUBRID_TEST_PASSWORD → CUBRID_TEST_URL → 기본값
localhost:33000/testdb, 사용자 dba, 빈 비밀번호).
SELECT 1 확인을 5초 간격으로 최대 30회 시도하며, 모두 실패하면
잡을 실패 처리해 테스트 단계가 실행되지 않습니다.
연결 및 읽기 제한 시간은 각각 5초이며 CUBRID_TEST_CONNECT_TIMEOUT과
CUBRID_TEST_READ_TIMEOUT으로 변경할 수 있습니다. 접속에 성공해도 SELECT 1이
실패하면 준비되지 않은 것으로 처리합니다. 성공/실패 및 커서 정리 오류 시에도
커서와 연결을 닫습니다.
통합 테스트는 파일 이름이나 고정된 테스트 수 대신 pytest 마커로 레인을 배정합니다:
| 레인 | 선택식 | 실행 워크플로 |
|---|---|---|
| 일반 | integration and not slow and not tls |
일반 PR/push CI, 전체 호환성 매트릭스, 주간 bug hunt |
| 장시간 | integration and slow and not tls |
주간/수동 bug hunt의 soak, chaos, 동시성 stress, 대량 준비된 배치 측정 |
| TLS | integration and tls |
일반 CI와 전체 워크플로의 전용 TLS 잡 |
| 공식 드라이버 차분 | integration and official_differential |
일반 CI와 전체 워크플로의 필수 official-differential 잡 (Python 3.11, CUBRID 10.2와 11.4) |
대량 준비된 배치 측정(#854)은 executemany(..., prepared=True)를 10,000행과
100,000행으로, 동기와 비동기에서 리스트와 한 번만 순회할 수 있는 제너레이터로 실행합니다.
오래 걸리므로 서버 하나를 대상으로 필요할 때 실행하세요. -s를 주면 측정값을 출력합니다.
스크립트만 실행하면 원하는 행 수를 지정하고 JSON으로 저장할 수 있습니다:
CUBRID_TEST_URL=cubrid://dba@localhost:33000/testdb \
pytest tests/test_integration_prepared_large_batch.py -m "integration and slow" -s
CUBRID_TEST_URL=cubrid://dba@localhost:33000/testdb \
python scripts/measure_prepared_batch.py --rows 10000 100000 --json prepared-batch.json
넓은 값과 두 연결 측정(#881)도 같은 방식으로 실행합니다. slow 테스트는 모든 값 프로필과
모드를 연결당 1,000행으로 실행하며, 스크립트는 --rows(연결당), --profiles, --modes를
받습니다:
CUBRID_TEST_URL=cubrid://dba@localhost:33000/testdb \
pytest tests/test_integration_prepared_wide_rows.py -m "integration and slow" -s
CUBRID_TEST_URL=cubrid://dba@localhost:33000/testdb \
python scripts/measure_prepared_wide_rows.py --rows 10000 --json prepared-wide-rows.json
기록된 결과는 PERFORMANCE.md를 참고하세요.
python scripts/check_integration_lanes.py는 현재 마커 목록을 수집하고 각 레인의 실제
워크플로 명령을 확인합니다. JUnit 검사(--results FILE)는 알려지지 않은 스킵,
빈 실행 또는 전체 스킵을 실패 처리합니다. 공식 드라이버 차분이 자기 레인 밖에서
스킵되는 경우는 official-lane-only로, /proc가 없는 플랫폼도 명시적으로
분류합니다. 브로커/TLS 설정 누락은 CI에서 허용하는 스킵이 아니며,
--lane official은 어떤 스킵도 허용하지 않습니다. 주간 bug hunt의 별도 오프라인 protocol, fault-broker,
placeholder 검사는 확장된 Hypothesis 프로필로 유지됩니다.
버전 차등 검사의 세션 격리 (#614). 하네스는 ping(reconnect=False)로
생존 여부를 확인하므로 복구가 끊긴 세션을 숨기지 않습니다. 끊긴 세션은
SessionLost를 발생시키기 전에 교체하여 다음 워크로드가 새 연결을 사용하게
하고, 임시 테스트 테이블의 추적 상태는 유지합니다. compare()는 모든
엔드포인트를 실행한 뒤 세션을 잃은 버전을 함께 보고합니다. 현재 테스트는
여전히 실패하며, 세션이 살아 있는 일반 SQL 오류는 연결을 교체하지 않습니다.
오프라인 회귀 검사는 tests/test_version_differential_isolation.py에 있습니다.
생성 문법은 다음 CUBRID 클라이언트 크래시 때문에 IF와 CASE WHEN에서
collection/numeric 분기 조합을 제외합니다.
#614의 원래 측정은
pycubrid 없이 csql로 빌드 10.2.18.9024-01b54fa, 11.0.16.0419-862b3af,
11.2.9.0866-ef544a1, 11.4.6.1963-0e7d3c1에서 SIGSEGV(종료 코드 139)를
확인했습니다. DB 서버는 살아 있습니다. 측정된 조건은 collection 옆의
scale >= 2 NUMERIC 분기를 선택하는 경우이며, 제외 규칙은 더 넓은 numeric
조합을 포함합니다. 직접 측정한 것은 IF뿐이며 CASE WHEN 제외는 예방적입니다.
원래 CI가 관찰한 CAS 세션 상실은 10.2입니다. 네 버전의 csql 재현은 네 버전
모두에서 CAS 상실을 직접 검증했다는 뜻이 아닙니다.
격리와 제외는 #618과
#668에서 병합되었습니다.
main 7fba209의 전체 매트릭스는
네 버전 차등 검사를 포함한 27개 작업을 모두 통과했습니다. 이는 완화된 레인의
검증이지 업스트림 크래시 수정이나 다른 치명적 문장이 없다는 증거는 아닙니다.
#675는 공식 Jira의
CBRD / Correct Error 접수를 별도로 추적하며, 이 후속 작업에서는 업스트림
티켓을 아직 접수하지 않았습니다. 검증된 수정 빌드가 가장 오래된 지원 CUBRID
빌드가 된 뒤에만 _is_fatal_conditional을 제거하세요. 유지되는 전송 연결 상실
계약은 연결 가이드를 참고하세요.
Bug-hunt 실패 진단 (#655). 주간/수동 대표 property-and-fault 잡은
명령별 junit_family=xunit1으로 normal-results.xml, slow-results.xml,
offline-results.xml을 만듭니다. 실패하면 scripts/collect_repro.py가 반복한
--junit 인자로 세 결과를 읽습니다. 기존 metadata.json 환경 필드는 유지하고
reports, failures, server_identity, collection_errors를 추가합니다.
누락·손상·읽기 오류·사례 0개·크기 초과 결과는 진단 상태로 명시하며 통과 증거로
취급하지 않습니다. XML 입력은 각각 10 MiB로 제한하고 원본 XML은 재현 묶음에
복사하지 않습니다.
실패·오류 기록은 확인된 파라미터 노드를 exact, 수집 오류의 파일만 있는
식별자를 file, 확인할 수 없는 사유를 unresolved로 남깁니다. 없거나 사용자
정의·불일치·가림 처리된 식별자를 클래스 이름에서 추측하지 않고, 광범위한
대체 재실행도 만들지 않습니다. reproduce.md는 shlex로 각 argv 토큰과 고정
환경 변수 할당의 값을 따로 인용하고, 확인된 대상을 중복 제거하며 비밀번호는 별도로
입력하도록 안내합니다. JUnit이 없을 때 허용되는 호출자의 기존 대상 정보는
검증된 결과 식별자가 아닌 힌트로 남습니다. 알려진 설정 비밀번호, 디코딩·인코딩된 URL 비밀번호와
인식하는 자격 증명 URL을 먼저 가린 뒤 실패 기록의 메시지·상세를 각각 UTF-8
바이트 64 KiB로 제한합니다. 2048자 이하의 유효한 CUBRID_TEST_URL은
user:***@host:port 형태로 기록합니다. urllib이 파싱하지 못하거나,
urllib이 파싱한 authority 밖에 @가 있거나(예: ://가 없는 u:pw@host/db나
cubrid:u:pw@host/db, 또는 /, ?, #, @를 포함한 비밀번호), 2048자를 넘으면
<unparseable-url-redacted>로 기록합니다. 이런 URL이나 탭·CR·LF(urllib이
제거함)를 포함한 URL은 각 @ 앞의 각 : 뒤 텍스트와, 그 텍스트를 /, ?,
#, @에서 나눈 각 조각과 접미사 중 3자 이상인 것을 원본 및 퍼센트
인코딩·디코딩 형태로 자유 텍스트에서 가립니다. 처음 2048자 안에서 첫 @ 앞의
첫 번째와 마지막 : 뒤 텍스트와, 마지막 @까지의 userinfo를 나눈 각
조각(접미사는 제외)은 항상 후보입니다. URL에서 나온 각 후보는 퍼센트 인코딩 없이
CR과 CRLF를 LF로 바꾼 형태(XML 파서가 요소 텍스트를 읽는 방식), 탭·CR·LF를
제거한 형태(urllib이 제거하는 방식), repr()로 이스케이프한 형태(트레이스백이 인용하는 방식)로도
가립니다. 3자 이상의 소문자 형태는 urllib이 소문자 호스트를 해석기와 테스트
게이트 텍스트에 넣는 방식(dba@***:33000/testdb)처럼 온전한 단어로 나타날 때만
가립니다. urllib은 포트를 다시 쓰고 IPv6 대괄호를 제거하며 %zone 앞 부분만
소문자로 바꾸므로(dba@***%Ab12:33000), 후보를 /, ?, #, @, %, [, ]에서
나누고 끝의 :port를 뺀 3자 이상의 호스트 형태 조각도 소문자 형태로 같은 방식으로
가립니다. 이렇게 파생된 조각은 관련 없는 진단 텍스트까지 과하게 가릴 수 있으며,
3자 미만의 파생 조각은 가리지 않습니다(#777). 2048자를 넘거나 후보 조각이
1024개 또는 후보 문자가 8192자를 넘게 될 URL은 한도 전까지 처음 2048자에서 찾은
후보를 유지하고, 전체 문자열과 함께 urllib이 파싱한 비밀번호와 URL 안의
scheme://user:password@ 형태 비밀번호도 가립니다. CUBRID_TEST_PASSWORD는
이런 변형 없이 원본과 퍼센트 인코딩 형태로 정확히 일치할 때만 가립니다. 두 한도는
각 후보를 추가하기 전에 확인하는 엄격한 상한이므로, 하나의 긴 꼬리 안에서도 한도에서
열거를 멈춥니다. 더 긴
단어 안에서 소문자로 바뀐 비밀번호 조각이나 다른 방식으로 변형된 비밀번호는 가린다고
보장하지 않습니다.
<unparseable-url-redacted>로 기록된 URL에는 엔드포인트 해석기를 신뢰하지
않습니다. urllib이 비밀번호 조각을 호스트(소문자로 변환)·사용자·데이터베이스에
넣을 수 있기 때문이며, 이때 재실행 명령과 서버 식별 정보는 unavailable로
기록합니다. 엔드포인트 필드와 준비 확인 엔드포인트는 알려진 비밀번호를 대소문자
구분 없이 포함하면 거부합니다. @가 없는 JDBC 형식 URL
(jdbc:cubrid:host:33000:db:u:PW:)은 자격 증명 URL로 인식하지 않으므로,
비밀번호를 CUBRID_TEST_PASSWORD에도 설정하지 않으면 적힌 그대로 기록됩니다.
엔드포인트 해석 오류는 해석기 텍스트가 아닌 고정 메시지로 보고합니다. 임의의
비밀이나 모든 데이터를 정화한다는 보장은 아닙니다.
선택적인 준비 확인 --server-info PATH는 실제 엔진 버전 또는 unavailable
사유를 기록합니다. 워크플로는 업로드 묶음 밖의 실행 ID·시도별 새 경로를 쓰며,
수집기는 기록된 SHA와 엔드포인트를 확인하고 이미지 태그·요청한 버전 계열을
관측한 실제 버전 대신 사용하지 않습니다. 새 버전 조회는 재접속이나 새 연결을
추가하지 않으며, 성공한 SELECT 1 준비 확인의 연결에서 커서 정리 전에 캡처한
물리 세대를 사용하고 재접속을 금지합니다. 기존 커서 닫기 동작은 그대로입니다.
그 정리가 세션을 교체하면 메타데이터 요청은 바뀐 세대를 거부하고 교체 세션을
관측하는 대신 unavailable로 남깁니다. 수집기 자체는 브로커에 접속하지 않습니다.
메타데이터 조회·쓰기 실패가 원래 준비 확인이나 pytest 결과를 바꾸지는 않습니다.
수집기 종료 코드 0은 최선의
진단 수집을 시도했다는 뜻이지 테스트 통과나 완전한 묶음의 보장이 아닙니다.
수집 오류 기록과 쓸 수 없었던 결과에 대한 stderr를 확인하세요.
저장된 Hypothesis DB는 여전히 바이너리 예제로 복사되며 자격 증명이 정화된다고 보장하지 않습니다. 외부 공유나 재현 지침 실행 전에 신뢰할 수 있는 검토가 필요합니다. 파일시스템 샌드박스, 보편적인 자격 증명 안전성이나 더 넓은 CI·이행 보장을 뜻하지 않습니다.
주간/수동 bug-hunt.yml의 downstream-corpus는 권고용 작업으로,
Python 3.12와 CUBRID 11.4의 독립된 세 셀을 실행합니다. 정확한 pycubrid
워크플로 커밋과 각 downstream 저장소의 현재 main 커밋을 별도 디렉터리에
체크아웃합니다. 모든 의존성 설치에 드라이버 Git 커밋 제약을 적용하고, 설치가
끝난 뒤 scripts/downstream_corpus.py가 다른 Git 출처/커밋, PyPI·로컬
교체, 소스 체크아웃이 설치 패키지 import를 가리는 상황을 거부합니다.
기존 scripts/wait_for_cubrid.py로 실제 브로커 준비 상태를 확인합니다.
| Downstream | 선택한 실사용 테스트 | 허용 스킵 |
|---|---|---|
sqlalchemy-cubrid |
ORM dogfood와 동기/비동기 풀 stress 파일을 별도 pytest 실행 | 없음 |
cubrid-mcp-server |
실제 도구 통합 사례와 별도로 선택한 공유 세션 동시 실행 사례 | 일반 mcp.xml 결과에서만 빈 스키마 스킵 허용; mcp-concurrency.xml에서는 없음 |
cubrid-cookbook-python |
MCP stdio를 포함한 AI-agent 스크립트 5개와 async-worker DB 작업을 별도 pytest 프로세스로 실행 | 없음 |
선택한 각 테스트 작업의 JUnit 결과에는 통과가 1건 이상 있어야 합니다.
결과 파일 누락, 테스트 실패, 예상 밖 스킵, 전체 스킵은 해당 권고용 셀을
실패시킵니다. 단계 요약과 실패 시에도 업로드하는 아티팩트에는 드라이버와
downstream 커밋, Python 및 설치된 패키지 버전, 드라이버 출처,
실제 CUBRID 서버 버전, 작업별 통과·스킵·실패 수를 기록합니다.
작업 단위 continue-on-error로
PR·릴리스 게이트에는 넣지 않지만, 증거에는 실패를 성공으로 표시하지 않습니다.
이는 downstream 전체 스위트가 아니라 제한된 실제 사용 표본입니다.
MCP 단계는 두 번째 pytest 프로세스에서
tests/test_integration.py::TestCubridIntegration::test_concurrent_tool_calls_serialize_shared_session
사례를 별도로 선택하여 mcp-concurrency.xml에 기록합니다. 이 결과에는
통과 사례가 있어야 하며 스킵, 실패, 오류는 모두 거부합니다. 일반 mcp.xml에서
허용하는 스킵 사유도 여기서는 허용하지 않습니다. 두 실행을 모두 수행하며,
어느 하나라도 pytest가 실패하면 실제 단계 결과를 실패로 기록합니다.
선택한 사례는 동일 프로세스에서 동시에 실행되는 MCP 핸들러가 기존 RLock을
통해 캐시된 하나의 물리적 Database 연결을 공유하는 동작을 검증합니다.
trace와 query 작업이 서로 끼어들지 않고, 응답이 각각 정확하며 실제 커서가
닫혀야 합니다. MCP stdio 동시 실행, 풀, 요청별 트랜잭션 격리, 처리량은
이 검증 범위에 포함하지 않습니다. SQLAlchemy는 별도의 풀 stress 작업을 제공합니다.
공식 드라이버 차분(#446)은 고정 소스에서 빌드한 공식 CUBRIDdb/_cubrid
드라이버와 pycubrid를 비교합니다. 로컬에서 재현하려면(Linux x86_64, git,
CMake 3.21 이상, C 컴파일러, Python 3.11 헤더 필요) 다음을 실행합니다.
python3.11 scripts/build_official_oracle.py --out .official-oracle
PYTHONPATH=.official-oracle PYCUBRID_OFFICIAL_ORACLE_REQUIRED=1 \
PYCUBRID_OFFICIAL_ORACLE_MANIFEST=.official-oracle/oracle.json \
PYCUBRID_DIFFERENTIAL_EVIDENCE=official-evidence.jsonl \
CUBRID_TEST_URL=cubrid://dba@localhost:33000/testdb \
python3.11 -m pytest tests/ -m "integration and official_differential"
python scripts/check_official_differential.py --evidence official-evidence.jsonl
새로 추가하거나 바꾼 주장은 tests/fixtures/official_differential_claims.json에
기록합니다. 해당 케이스는 tests/test_official_differential.py의 CASES에
추가한 뒤 python scripts/check_official_differential.py --write-docs를
실행합니다. 차이는 수정하거나, 이유·이슈·양쪽 관측값을 갖춘 deviation으로
기록합니다. 현재 출력에 맞추려고 기대값만 고치지 마세요.
호환성 가이드를 참고하세요.
tests/test_protocol_fuzz.py는 tests/helpers/cas_reply.py가 만든 실제와 같은
브로커 응답을 변형합니다(#523). 모든 주요 타입의 컬럼 메타데이터와 행 데이터를
담은 실행 응답과 FETCH 응답, 그리고 스키마, 배치, LOB 응답이 포함됩니다. 각 시드는
디코딩 기댓값과 길이 워드, 개수, 필드 경계의 오프셋을 기록하므로 변형하지 않은
시드는 정확한 왕복 검사가 되고, 변형은 잘림과 길이/개수 불일치를 겨냥합니다.
새 컬럼 조합을 시드로 추가하려면 RESULT_SETS에 ResultSet을 추가하세요. FETCH와
실행 응답 fuzz 대상이 이를 가져옵니다. 새 응답 빌더에는 별도의 왕복 테스트와 fuzz
대상이 필요합니다. 예제 수는 Hypothesis 프로필에서
정합니다(pr: 대상마다 50개, 모듈 전체 약 2초; nightly: 1000개).
문서 예외와 기여자 검증 기록¶
문서 게이트는 따옴표 인용, HTML 주석, 코드 블록 밖의 독립된 물리적 소스 줄에 실제
이유가 있는 Docs: not needed -만 인정합니다. 일반 설명 바로 옆 줄에 둘 수 있으며,
별도 문단이나 빈 줄이 필수인 것은 아닙니다.
앞의 공백은 0~3개까지 허용하며, 들여쓴 코드와 HTML 인용/pre/code 블록의 예시는
예외 승인을 부여하지 않습니다.
기존 docs-not-needed 라벨 예외는 별도로 유지됩니다. make docs-reason-check는
헬퍼 doctest와 실제 이벤트 JSON 기반 워크플로 사례를 실행하며, make check와
docs-sync CI에서도 이 검사를 실행합니다.
기여자는 실제 실행한 명령/결과와 실행하지 않은 검사/이유를 기록하고, 선택적 AI
리뷰는 별도로 구분합니다. 유지보수자는 내부 리뷰, 실제 이슈 라벨 및 명시적으로
승인한 translations-deferred 후속 작업을 조율합니다. PR 본문의 번역 도움 요청은
승인을 부여하지 않습니다. 한국어 README 동기화는 필수이며 다른 번역은 권고 수준입니다.
docs/ 아래 영어 문서가 기준이고, docs/ko/ 아래 한국어는 완전하게 유지하는 유일한
번역입니다. python scripts/check_docs_translation.py(린트 잡에서 실행)는 모든
docs/<name>.md를 docs/ko/<name>.md와 비교해, 한국어 파일이 없거나 제목(2~4단계),
펜스 코드 블록, 표 행의 개수가 다르면 실패합니다. 영어 섹션을 추가할 때는 같은
PR에서 한국어 섹션도 추가하세요. 의도적으로 영어로만 두는 문서는 그 스크립트의
EXCEPTIONS에 사유와 함께 넣습니다. 이 검사는 구조를 비교할 뿐 문구는 보지 않습니다.
공유 doc-lint와 CodeQL 호출은 workflow_call 입력을 확인한 검토된 커밋 SHA를
사용합니다. 기존 권한, 권고 수준 롤아웃 및 필수 게이트는 유지됩니다. doc-lint가
main 기반 설정/스캐너를 내려받으므로 호출자 핀만으로 이 자산까지 고정되지는 않습니다.
GitHub Actions 워크플로¶
| 워크플로 | 트리거 | 설명 |
|---|---|---|
ci.yml |
PR, main 푸시, 주간, 수동 실행 | 최소 PR 스모크; main/주간 커버리지와 대표 통합 검사 |
integration-full.yml |
수동 실행, publish-pypi.yml에서 호출 |
전체 Python × CUBRID 호환성 매트릭스 |
release-please.yml |
main push 또는 수동 실행 | PR 전용 릴리스 후보 생성/갱신 (버전 + 생성/큐레이션된 노트) |
publish-pypi.yml |
main 푸시, 복구용 수동 실행 | 병합된 릴리스 PR 감지 후 전체 매트릭스, 빌드, 태그 + GitHub Release + PyPI, cookbook 검증 |
CI 매트릭스¶
일상 CI는 전체 매트릭스 대신 대표 오프라인·통합 조합을 사용합니다. 일반 PR은 Ubuntu/Python 3.12에서 오프라인 스모크 검사를 실행합니다. 고위험 PR은 지원 범위의 최저·최신 Python(3.11, 3.15)에서 전체 오프라인 회귀 검사(커버리지 제외)를 실행하고 Python 3.14/CUBRID 11.4 통합 조합을 선택합니다. main, 최근 변경이 있는 주간 실행과 수동 실행은 Python 3.11과 3.15에서 전체 오프라인 검사와 95% 커버리지를 유지하고 Python 3.11/CUBRID 10.2 통합 조합을 추가합니다. 저장소 도구 검사는 관련 경로에 따라 Linux 단일 레인에서 실행합니다. 전체 통합 검사는 명시적 수동 실행과 릴리스에서 유지합니다. 정확한 선택 조건과 검증 요건은 CI 실행 정책을 참고하세요.
CI 라이브 작업과 Make 계약¶
기여자는 make integration(루트 docker-compose.yml, CUBRID 서버 하나,
-m "integration and not tls")과 make integration-tls로 라이브 테스트를 실행합니다.
라이브 CUBRID에 접속하는 CI 작업은 이 타깃 중 하나를 실행하거나, 워크플로에
# Not using make integration because ... 주석을 두고 아래 표에 행을 둡니다.
tests/test_workflow_make_contract.py가 두 조건을 모두 검사합니다(#1023). 작업은
CUBRID 버전과 매트릭스, pytest 선택, 레인 감사, 결과 파일과 아티팩트, 준비 대기,
권한이 그대로일 때만 전환합니다. 현재 이를 만족하는 작업은 없으므로 모든 라이브 작업은
자체 서비스 컨테이너와 단계를 유지합니다.
| 워크플로 | 작업 | 결정 | Make 계약과 같지 않은 이유 |
|---|---|---|---|
ci.yml |
integration-tests |
유지 | integration and not slow and not tls를 선택하지만 make integration은 slow 테스트도 실행합니다. 실행 전 레인 감사, Python × CUBRID 매트릭스, 단계 전 GitHub 서비스 헬스 체크를 사용합니다 |
integration-full.yml |
integration-full |
유지 | integration-tests와 같은 선택과 감사를, 요청된 SHA에서 Python 5 × CUBRID 4 릴리스 매트릭스로 실행합니다 |
ci.yml |
integration-tls |
유지 | 생성한 인증서와 SSL=OFF 포트 30000을 갖춘 SSL=ON 브로커를 준비하고 tests/ 전체에서 integration and tls를 실행합니다. Compose 서비스에는 SSL 브로커가 없고 make integration-tls는 tests/test_aio_ssl_integration.py만 실행합니다 |
integration-full.yml |
integration-tls |
유지 | ci.yml TLS 작업과 같으며 전체 TLS 매트릭스로 실행합니다 |
ci.yml |
integration-charset |
유지 | Compose 서비스가 설정하지 않는 EUC-KR 데이터베이스(CUBRID_LOCALE=ko_KR.euckr)가 필요하고 tests/test_integration_charset.py만 실행합니다 |
integration-full.yml |
integration-charset |
유지 | ci.yml charset 작업과 같습니다 |
ci.yml |
official-differential |
유지 | CUBRID 10.2와 11.4를 동시에(포트 33102, 33114) 띄우고 고정된 공식 oracle 빌드가 필요합니다. Compose는 서버 하나만 시작합니다 |
integration-full.yml |
official-differential |
유지 | ci.yml 공식 differential과 같습니다 |
integration-full.yml |
version-differential |
유지 | CUBRID 10.2, 11.0, 11.2, 11.4를 동시에 띄워야 합니다(CUBRID_VERSION_MATRIX) |
bug-hunt.yml |
property-and-fault |
유지 | 주간 광역 Hypothesis 프로필, 별도의 slow soak/chaos 레인, xunit1 JUnit 파일, 서버 정보를 포함한 repro 수집을 사용합니다 |
bug-hunt.yml |
perf-trend |
유지 | integration 마커가 아니라 캐시된 기준선에 대해 tests/test_benchmarks.py를 실행합니다 |
bug-hunt.yml |
downstream-corpus |
유지 | 다운스트림 저장소의 자체 테스트를 정확한 SHA의 드라이버로 실행합니다 |
릴리스를 검증하는 publish-pypi.yml 작업은 직접 CUBRID를 실행하지 않습니다. matrix는
위에서 다룬 integration-full.yml을 호출하고, verify-candidate와 verify-cookbook은
cookbook의 smoke-test.yml을 호출합니다.
PR 검증 비용¶
현재 CI 비용 근거는 CI 실행 정책에 있습니다. 이벤트 계층별 러너 시간, 잡 수, 실패 검출률을 재현 가능한 정의와 함께 기록하고, 커버리지 소유 표도 함께 둡니다(#750, #781). CI 비용을 판단할 때는 이전 수치가 아니라 이 수치를 사용하세요.
이전 #564 기준선(2026-10-01)은 지금은 존재하지 않는 워크플로를 측정했습니다. Python 3.10을 포함한 2 OS × 5 Python 오프라인 매트릭스와 pip 캐시를 쓰던 구성입니다. 이 기준선은 #564와 이 파일의 git 이력에 남아 있습니다. 그 결과 중 두 가지는 지금도 유효합니다.
scripts/wait_for_cubrid.py를 변경하면 코드 경로 통합 검사가 선택됩니다 (tests/test_workflow_path_impact.py).- 저장소 도구 테스트가 실패·취소된 합성 결과로
CI Gate를 실행합니다.
의존성 설치는 이후 uv로 바뀌었습니다(의존성 설치).
문서 구조¶
이 사이트는 세 부류의 독자를 위한 것이며, 각 페이지는 다음 역할 중 하나를 맡습니다.
- 학습 경로(여기서 시작): 처음 쓰는 사람이 하나의 이어지는 예제로, 단원마다 동작 하나씩 차례로 배웁니다.
- 사용자 참고 문서(사용, 참고): 경험 있는 사용자가 옵션, API 계약, 타입, 지원 범위를 찾아봅니다.
- 도움말(도움말): 문제가 생긴 사용자가 문제 해결, 자주 묻는 질문, 기존의 버그와 취약점 제보 경로를 찾습니다.
- 개발 자료(개발과 기여): 기여자가 프로토콜, 설계, 검증 근거를 찾습니다.
아래 표는 영향을 받는 각 페이지의 독자, 그 페이지가 답하는 질문, 앞뒤에 오는 페이지, 결정 사항을
기록합니다. "원본"은 직접 편집하는 Markdown 파일이고, 사이트 URL은 사이트 루트 기준입니다.
docs/ko/<page>는 같은 구조로 ko/<page>/에서 제공됩니다. docs/llms-full.txt와 루트의
llms.txt는 scripts/generate_llms_full.py가 생성하므로 직접 편집하지 마세요.
| 원본과 사이트 URL | 독자와 질문 | 앞 페이지 | 다음 페이지 | 결정 |
|---|---|---|---|---|
docs/index.md, / |
누구나: pycubrid는 무엇이고 어디서 시작하나요? | 없음 | quickstart.md |
유지합니다. |
README.md(저장소와 PyPI 페이지, 사이트에는 빌드되지 않음) |
방문자: 어떤 프로젝트인가요? | 없음 | quickstart.md |
유지합니다. 시작 페이지로 가는 링크는 그대로 유효합니다. |
docs/quickstart.md, /quickstart/ |
처음 쓰는 사람: 데이터베이스와 Python을 어떻게 준비하나요? | Python 3.11 이상, 그리고 Docker 또는 기존 데이터베이스 | first-query.md |
하나뿐인 표준 시작 페이지(1단원, 준비)로 유지합니다. 2단계, 3단계, 정리하기는 단원으로 안내하는 짧은 안내 구역이며, 기존 제목을 모두 남겨 예전 딥 링크가 계속 도착합니다. 첫 연결 오류 표는 여기에 남깁니다. |
docs/first-query.md, /first-query/ |
처음 쓰는 사람: 연결하고, 문장 하나를 실행하고, 행 하나를 읽으려면? | quickstart.md |
save-a-row.md |
2단원입니다. |
docs/save-a-row.md, /save-a-row/ |
처음 쓰는 사람: 테이블을 만들고 행 하나를 저장해 다른 연결에서 보려면? | first-query.md |
commit-and-rollback.md |
3단원이며, 퀵스타트 3단계에서 나눴습니다. |
docs/commit-and-rollback.md, /commit-and-rollback/ |
처음 쓰는 사람: 트랜잭션은 무엇을 남기고 무엇을 버리나요? | save-a-row.md |
finish-safely.md |
4단원이며, 퀵스타트 3단계에서 나눴습니다. |
docs/finish-safely.md, /finish-safely/ |
처음 쓰는 사람: 만든 것을 제거하고, 중단되었거나 반복된 실행에서 복구하려면? | commit-and-rollback.md |
EXAMPLES.md, CONNECTION.md, 도움말 |
5단원이며, 퀵스타트 정리하기에서 나눴고 테이블 소유권 확인과 경고는 그대로 둡니다. |
docs/EXAMPLES.md, /EXAMPLES/ |
사용자: 흔한 작업을 어떻게 하나요? | 단원들 | 참고 문서 | 사용에 유지합니다. |
docs/CONNECTION.md, /CONNECTION/ |
사용자: 어떤 연결 옵션, URL, 타임아웃, TLS 설정이 있나요? | 단원들 | API_REFERENCE.md |
파일과 앵커를 유지하고 단원에서 해당 절로 링크합니다. 참고에 두고 연결 옵션이라는 이름을 붙입니다. |
docs/PARAMETER_BINDING.md, /PARAMETER_BINDING/ |
사용자: 값을 어떻게 안전하게 전달하고, 무엇은 보장되지 않나요? | 단원들 | TYPES.md |
유지합니다. 참고에 두고 값을 안전하게 전달하기라는 이름을 붙입니다. |
docs/TYPES.md, docs/API_REFERENCE.md, docs/SUPPORT_MATRIX.md |
사용자: 어떤 타입, 호출, 버전이 지원되나요? | 어느 페이지든 | 어느 페이지든 | 참고에 유지합니다. |
docs/CAS_OVERVIEW.md, /CAS_OVERVIEW/ |
숙련 사용자: 브로커, CAS 프로세스, CAS 와이어 프로토콜, CCI는 무엇이고, DB-API 호출은 프로토콜의 어디와 만나나요? | 참고 페이지 | PROTOCOL.md, ARCHITECTURE.md |
참고에 심화로 둡니다. PROTOCOL.md, ARCHITECTURE.md, CONNECTION.md를 복사하지 않고 링크하는 개념 개요입니다. |
docs/TROUBLESHOOTING.md, docs/faq.md |
문제가 생긴 사용자: 이 오류는 무슨 뜻인가요? | 어느 페이지든 | SECURITY.md와 이슈 양식의 제보 안내 |
파일을 유지하고 도움말에 두며, 이슈 양식과 SECURITY.md로 가는 링크를 함께 둡니다. |
docs/PROTOCOL.md, docs/ARCHITECTURE.md, docs/UPSTREAM_TEST_LEDGER.md, docs/UPSTREAM_COMPATIBILITY.md, docs/PREPARED_BINDING_DESIGN.md |
기여자: 드라이버는 어떻게 동작하고, 무엇이 그 근거인가요? | DEVELOPMENT.md |
어느 페이지든 | 파일을 유지하고 탐색에서 개발과 기여에 둡니다(참고에서 옮김). |
docs/DEVELOPMENT.md, docs/CI_POLICY.md, docs/PRD.md, docs/PERFORMANCE.md |
기여자나 운영자: 어떻게 빌드, 테스트, 측정, 릴리스하나요? | 어느 페이지든 | 어느 페이지든 | 유지합니다. 개발과 기여에 두되, 성능 가이드는 운영에 남깁니다. |
단원 작성하기¶
여기서 시작 아래의 단원은 동작 하나를 가르칩니다. 각 단원에는 다음 부분을 이 순서로 두고, 한국어 페이지도 같은 구조를 유지하세요.
- 목표: 독자가 무엇을 하는지, 그리고 어느 단원 다음인지 한두 문장으로 씁니다.
- 시작하기 전에: 앞 단원이 만들어 두었어야 하는 것(출력된 결과, 테이블, 활성화된 가상 환경)을 씁니다. 열려 있는 Python 세션이나 이전 노트북을 가정하지 마세요. 모든 프로그램은 터미널에서 실행하는 파일입니다.
- 프로그램이나 명령: 파일 이름, 전체 소스, 그리고 그것을 실행하는 명령을 씁니다.
- 예상 결과: 일회용 CUBRID 서버에서 확인한 정확한 출력을 씁니다.
- 설명: 새로 쓴 호출이 각각 무엇을 했는지 쓰고, 전체 계약이 있는 참고 문서 절로 링크합니다. 그 동작에 해당하는 타임아웃, 재시도, 보안 제한은 참고 문서에만 두지 말고 여기에 씁니다.
- 다음 단계: 다음 단원으로 가는 링크를 둡니다.
반복되거나 중단된 실행의 복구는 자원을 만드는 단원이나 안전하게 마치기에 둡니다. 정리는 단원이 만든 것으로만 한정하고, 소유권 확인을 프로그램 안에 보이게 두어야 합니다. 프로그램이나 명령이 바뀌면 일회용 서버에서 전체 순서를 다시 실행하고, 실행한 플랫폼을 기록하세요.
아키텍처 개요¶
graph TD
user[User Code] --> init[__init__.py - Module API connect/types/exceptions]
init --> connection[connection.py - TCP socket, CAS handshake, session]
connection --> cursor[cursor.py - SQL execution, parameter binding, fetch]
cursor --> protocol[protocol.py - CAS packet classes serialize/deserialize]
protocol --> packet[packet.py - PacketWriter + PacketReader binary I/O]
packet --> constants[constants.py - CAS function codes, data types, enums]
types[types.py - PEP 249 types] --> cursor
exceptions[exceptions.py - PEP 249 errors] --> connection
lob[lob.py - LOB objects] --> connection
데이터 흐름¶
- 사용자가
cursor.execute("SELECT ...")호출 - Cursor가 파라미터를 바인딩하고
PrepareAndExecutePacket생성 - Connection이
_send_and_receive(packet)호출 - PacketWriter가 프로토콜 헤더로 요청 직렬화
- Socket이 CAS 서버로 바이트 전송
- Socket이 응답 바이트 수신
- PacketReader가 응답 역직렬화
- Packet이 컬럼 메타데이터와 행 데이터 파싱
- Cursor가
fetchone()/fetchall()을 위해 행 저장
새 패킷 타입 추가¶
새 CAS 함수를 추가하려면:
constants.py의CASFunctionCode에 함수 코드 추가:
protocol.py에 패킷 클래스 생성:
class MyNewPacket:
"""Description (FC=42)."""
def __init__(self, arg1: int) -> None:
self.arg1 = arg1
self.result: str = ""
def write(self, cas_info: bytes) -> bytes:
writer = PacketWriter()
writer._write_byte(CASFunctionCode.MY_NEW_FUNCTION)
writer.add_int(self.arg1)
payload = writer.to_bytes()
header = build_protocol_header(len(payload), cas_info)
return header + payload
def parse(self, data: bytes) -> None:
reader = PacketReader(data)
_ = reader._parse_bytes(DataSize.CAS_INFO)
response_code = reader._parse_int()
if response_code < 0:
remaining = len(data) - 8
_raise_error(reader, remaining)
# 결과별 데이터 파싱
self.result = reader._parse_null_terminated_string(response_code)
tests/test_protocol.py에 테스트 추가:
def test_my_new_packet_write():
packet = MyNewPacket(arg1=42)
data = packet.write(b"\x00\x00\x00\x00")
assert len(data) > 8 # 헤더 + 페이로드
새 데이터 타입 추가¶
새 CUBRID 데이터 타입을 지원하려면:
constants.py의CUBRIDDataType에 타입 코드 추가protocol.py의_read_value()에 리더 추가packet.py의PacketWriter에 라이터 추가 (필요 시)- 읽기와 쓰기 양쪽에 테스트 추가
릴리스 절차¶
릴리스는 유지보수자 전용이며 RELEASING.md를 따릅니다:
release-please.yml이 릴리스 PR(버전 갱신 + 날짜가 있는 CHANGELOG 섹션, make release-check VERSION=X.Y.Z로 확인)을
엽니다. 검토 후 squash 병합하면 publish-pypi.yml이 전체 매트릭스, 한 번의 빌드, 태그, PyPI 게시, cookbook 검증을
자동으로 수행합니다. 태그 푸시나 게시를 수동으로 하지 않습니다.
변경 이력 검증¶
python scripts/lint_changelog.py는 릴리스별 중복 하위 섹션을 거부합니다. Unreleased만 있는 파일도 검사하지만, 섹션 정책과 마찬가지로 [Unreleased]와 1.10.0 이후 릴리스에만 적용하며 1.10.0까지의 릴리스 기록은 다시 쓰지 않습니다. 서로 다른 릴리스는 같은 하위 섹션 이름을 사용할 수 있습니다. 펜스 코드 블록은 scripts/lint_changelog.py와 scripts/compose_release_changelog.py에서 같은 규칙을 따릅니다. 펜스 안의 ### 줄은 제목이 아니라 항목 내용이고, 펜스 안의 ## [ 릴리스 헤더는 오류이며(scripts/extract_release_notes.py는 펜스를 인식하지 못해 Release 본문이 잘립니다), 닫히지 않은 펜스도 오류입니다. 0번째 열에서 시작하는 백틱 3개 펜스만 인식하며 물결표(~~~)나 들여쓰기/중첩 펜스는 인식하지 않습니다. 단 하나의 예외는 CHANGELOG.md와 릴리스 PR 본문에서 생성된 이슈 참조(, closes [#N]를 , refs [#N]로)를 다시 쓰는 composer의 처리입니다. 이 처리는 다시 쓰지 않을 곳만 정하고 PR 본문은 린트하지 않으므로, 물결표 펜스, 3칸까지 들여쓴 펜스, 더 긴 펜스를 포함해 GitHub가 코드로 표시하는 모든 펜스를 건너뜁니다. 두 스크립트는 sqlalchemy-cubrid, cubrid-mcp-server, cookbook과 공유하며, 네 저장소 모두 규칙 5의 cutoff 적용을 도입한 뒤에는 SECTION_POLICY_CUTOFF만 다릅니다.
[Unreleased]와 1.10.0 이후 릴리스에서는 내용이 있는 표준 ### 섹션만 다음 순서로 허용합니다: Upgrade notes,
Added, Changed, Deprecated, Removed, Fixed, Security, Performance, Documentation, CI, Tests. Docs가 아니라
Documentation을 쓰고, 릴리스 자동화 항목은 CI 또는 Changed에 둡니다. 1.10.0까지의 릴리스는 기존 제목을
유지합니다. GitHub Release 본문은 CHANGELOG 섹션에 **Full Changelog** 비교 링크 하나를 붙인 것입니다.
AGENTS.md의 "GitHub Release Policy"를 참고하세요.