Skip to content

CUBRID-Python 드라이버 호환성 (한국어)

🌐 DRIVER_COMPAT.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.

이 문서는 sqlalchemy-cubrid와 CUBRID Python 드라이버(CUBRIDdb), CUBRID 서버 버전 간의 테스트된 호환성 매트릭스를 설명합니다.


목차

데이터베이스와 드라이버 버전 구분

CUBRID Python 드라이버의 릴리스 버전은 CUBRID Database 제품 버전과 비슷하게 보일 수 있습니다. 관련 릴리스 계열의 이름을 따른 것으로 해석할 수 있지만, 각 번호가 가리키는 구성 요소는 서로 다릅니다. 주·부 버전이 같아야 접속할 수 있다는 뜻도 아니며, 숫자가 비슷하다고 모든 조합의 호환성이 보장되는 것도 아닙니다.

구분 예시 버전이 의미하는 대상
CUBRID 데이터베이스 서버 11.4.1 실행 중인 DBMS 버전. SELECT VERSION()으로 확인
CUBRIDdb 공식 소스 릴리스 v11.3.0.51 Python C 확장 드라이버의 GitHub 소스 릴리스. 서버 버전도, PyPI의 11.3 패키지도 아님
CCI 클라이언트 라이브러리 11.2.0 해당 드라이버 릴리스에 사용된 네이티브 C 클라이언트 인터페이스
PyPI 배포판 릴리스 CUBRID-Python 9.3.0 PyPI에 등록된 C 확장 드라이버 패키지 릴리스 버전
PyPI 소스 압축 파일 식별자 9.3.0.0001, 9.3.0.0002 2014년과 2015년에 각각 업로드된, 같은 PyPI 9.3.0 릴리스 내 두 소스 파일의 이름
설치된 C 확장 모듈 버전 예: 11.3.0.0001 빌드된 _cubrid.__version__ 값. 네 번째 부분은 Git 커밋 수로 생성될 수 있어 공식 소스 태그의 끝자리와 다를 수 있음
SQLAlchemy Dialect 버전 예: sqlalchemy-cubrid 1.x 이 저장소에서 별도로 배포하는 Python 패키지 버전

2025년 9월 8일 게시된 공식 CUBRID Python v11.3.0.51 릴리스 노트는 CCI 11.2.0으로 빌드하여 CUBRID 서버 11.4.1과의 적합성을 확인했다고 밝힙니다. 한 릴리스 설명 안에서도 버전 번호가 서로 다르게 나타나는 사례입니다. 해당 GitHub 릴리스에서는 소스 아카이브를 받을 수 있지만 별도 바이너리 첨부 파일은 없으며, PyPI에 CUBRID-Python 11.3으로 게시된 릴리스도 아닙니다.

PyPI의 CUBRID-Python 프로젝트는 9.3.0 패키지를 CUBRID Database 9.3.0용 커넥터로 설명합니다. 이는 제품 버전과 연계된 당시의 명명 방식에 관한 자료이지, 다른 서버에 연결할 수 없다는 증거는 아닙니다. 과거 9.3.0.1 드라이버와 CUBRID 11.4 조합의 테스트에서는 연결은 가능했으나 BIGINT 반환 타입 차이가 확인되었습니다. 지원 정책, 연결 가능 여부, 실제 검증 조합, 알려진 제약은 구분해서 기록해야 합니다.

연결 구조

flowchart TD
    app["Application / SQLAlchemy Core & ORM"] --> dialect["sqlalchemy-cubrid dialect"]
    dialect --> pure["pycubrid / pycubrid.aio"]
    dialect --> native["CUBRIDdb (_cubrid C extension)"]
    native --> cci["CCI client library"]
    cci --> cas["CUBRID Broker / CAS"]
    pure --> cas
    cas --> database["CUBRID database server"]

[cubrid], [cubriddb], [pycubrid] extra는 설치할 의존성을 고르는 옵션이지 데이터베이스 서버 버전이 아닙니다. cubrid://와 cubrid+cubriddb://는 CUBRIDdb를, cubrid+pycubrid://와 cubrid+aiopycubrid://는 각각 pycubrid 드라이버를 선택합니다. 이 URL에는 드라이버 릴리스 버전이 들어 있지 않습니다.

환경에서 실제 버전 확인

from importlib import import_module
from importlib.metadata import PackageNotFoundError, version

for name in ("sqlalchemy-cubrid", "pycubrid", "CUBRID-Python"):
    try:
        print(f"{name}: {version(name)}")
    except PackageNotFoundError:
        print(f"{name}: not installed")

try:
    cubriddb = import_module("CUBRIDdb")
except ImportError:
    print("CUBRIDdb C extension: not importable")
else:
    extension = getattr(cubriddb, "_cubrid", None)
    print("CUBRIDdb C extension:", getattr(extension, "__version__", "unknown"))

CUBRID-Python과 cubrid_python은 배포판 이름을 정규화한 표기 차이이며 서로 다른 두 드라이버가 아닙니다. 실행 중인 데이터베이스 서버 버전은 실제 연결에서 SELECT VERSION()을 실행해 확인합니다. CUBRIDdb를 소스에서 직접 빌드했다면 사용한 Git 태그 또는 커밋도 별도로 남깁니다. 설치된 패키지 버전만으로 서버 버전이나 전체 기능 호환성을 추정해서는 안 됩니다.


드라이버 개요

속성 값
지원하는 설치 방법 cubrid-python v11.3.0.51 이상을 소스에서 빌드 (방법)
PyPI 패키지 CUBRID-Python, 최신 릴리스 9.3.x(2015년): 테스트되지 않음, 지원하지 않음 (상세)
임포트 이름 CUBRIDdb
타입 C 확장 (CPython 전용)
DBAPI 수준 DB-API 2.0 (PEP 249)
파라미터 방식 qmark
소스 github.com/CUBRID/cubrid-python

이 드라이버는 CUBRID CCI(C Client Interface) 라이브러리를 감쌉니다. 순수 Python 드라이버가 아니며 CCI 헤더에 대한 컴파일이 필요합니다. 위의 소스 빌드 CUBRIDdb 드라이버는 이 방언에서 지원합니다. 폐기 예정인 [cubrid] / [cubriddb] 설치 extra가 테스트되지 않은 PyPI 패키지를 선택하는 것이며, 드라이버 인터페이스 자체가 폐기된 것은 아닙니다. 새 프로젝트에는 CUBRID 네이티브 라이브러리가 필요 없고 아래 CUBRIDdb 제약을 피할 수 있는 pycubrid를 권장합니다.


버전 호환성 매트릭스

테스트된 구성

sqlalchemy-cubrid CUBRID-Python 드라이버 CUBRID 서버 Python 상태
0.4.0 v11.3.0.51 11.4 3.10 – 3.14 ✅ CI에서 테스트
0.4.0 v11.3.0.51 11.2 3.10 – 3.14 ✅ CI에서 테스트
0.4.0 v11.3.0.51 11.0 3.10 – 3.14 ✅ CI에서 테스트
0.4.0 v11.3.0.51 10.2 3.10 – 3.14 ✅ CI에서 테스트
0.3.x v11.3.0.51 10.2 – 11.4 3.10 – 3.13 ✅ 테스트됨
전체 PyPI CUBRID-Python 9.3.x 전체 전체 ❌ 테스트되지 않음, 지원하지 않음 (상세)

CUBRID 서버 버전 지원

서버 버전 드라이버 버전 비고
11.4 v11.3.0.51 최신 안정
11.2 v11.3.0.51 LTS 릴리스
11.0 v11.3.0.51 레거시 지원
10.2 v11.3.0.51 최소 지원
12.x — 미출시. 출시되면 테스트 예정

Python 버전 지원

Python 상태 비고
3.11 ✅ 완전 지원 최소 버전
3.12 ✅ 완전 지원
3.13 ✅ 완전 지원
3.14 ✅ 완전 지원
3.15 ✅ 완전 지원 표준 GIL 빌드. GCC 14에서는 Python 3.14와 3.15 모두에서 기본 cubrid-python 빌드가 실패합니다(아래 참고)
3.10 ❌ 1.10.0부터 미지원 1.9.x까지 지원

GCC 14와 공식 드라이버 빌드: cubrid-python v11.3.0.51은 ubuntu-latest에서 CI 빌드 절차로 빌드됩니다. 이 환경의 컴파일러는 python_cubrid.c의 -Wincompatible-pointer-types와 -Wint-conversion 진단을 경고로 보고합니다. GCC 14는 이를 기본적으로 오류로 만들기 때문에 같은 절차가 Python 3.14와 3.15 모두에서 컴파일에 실패합니다. Python 3.15가 아니라 도구 체인의 변화입니다. 소스를 바꾸지 않고 CFLAGS="-Wno-error=incompatible-pointer-types -Wno-error=int-conversion"로 빌드한 기여자 빌드는 동작하는 3.15 wheel을 만들었습니다(#666에 기록). 이 프로젝트는 CI에 이 플래그를 추가하지 않습니다. 러너 이미지가 GCC 14로 바뀌면 먼저 이 결정이 필요합니다.


예외 계층

CUBRIDdb 11.3.0.51(모듈 _cubrid)은 Warning을 제외한 모든 PEP 249 예외 클래스를 제공합니다:

graph TD
    base["BaseException"] --> exc["Exception"]
    exc --> err["CUBRIDdb.Error (Base DBAPI error)"]
    err --> iface["CUBRIDdb.InterfaceError (Driver-level errors)"]
    err --> db["CUBRIDdb.DatabaseError (Server-level errors)"]
    db --> data["CUBRIDdb.DataError"]
    db --> op["CUBRIDdb.OperationalError"]
    db --> integ["CUBRIDdb.IntegrityError"]
    db --> internal["CUBRIDdb.InternalError"]
    db --> prog["CUBRIDdb.ProgrammingError"]
    db --> ns["CUBRIDdb.NotSupportedError (Unsupported operations)"]

서버 오류가 어떤 클래스가 되는지는 드라이버의 오류 코드 매핑이 결정합니다. CUBRID 11.4에서 관찰한 결과:

오류 (네이티브 코드) CUBRIDdb 클래스
구문 오류 또는 알 수 없는 테이블 (-493) ProgrammingError
NOT NULL (-631), 외래 키 (-922), 고유 (-670) IntegrityError
0으로 나누기 (-494) IntegrityError
외래 키로 제한된 부모 DELETE/UPDATE (-924) IntegrityError
참조되는 테이블의 TRUNCATE (-1284, CUBRID 10.2 및 11.0은 -924 IntegrityError를 보고) DatabaseError
실패한 CAST (-181) DatabaseError
결과의 0 DATE/DATETIME ('0000-00-00') DB-API 오류가 아닌 SystemError: SQLAlchemy가 감싸지 않으며 연결은 계속 사용할 수 있음
rollback() 이후 결과 읽기 (CCI -20040) InterfaceError

SQLAlchemy는 전달받은 클래스를 감싸므로 cubrid://는 제약 위반에 sqlalchemy.exc.IntegrityError를 발생시킵니다. pycubrid는 알려진 문제 9를 참고하세요. sqlalchemy-cubrid 방언은 연결 해제 오류를 다른 실패와 구별하기 위해 문자열 기반 메시지 매칭을 사용합니다.


드라이버 API 사용

방언이 의존하는 드라이버 전용 API:

연결 메서드

메서드 용도 사용처
conn.ping() 연결 생존 확인 CubridDialect.do_ping()
conn.get_last_insert_id() auto-increment 값 조회 CubridExecutionContext.get_lastrowid()
conn.set_autocommit(bool) / conn.autocommit 오토커밋 제어 CubridDialect.on_connect(), set_isolation_level()(AUTOCOMMIT 전환과 복귀), detect_autocommit_setting()
conn.cursor() 커서 생성 표준 DB-API

오류 코드 추출

# 오류 코드는 exception.args[0]에 있음
try:
    cursor.execute("invalid sql")
except CUBRIDdb.DatabaseError as e:
    code = e.args[0]  # int

방언의 _extract_error_code()는 정수 args[0]만 읽습니다. 문자열 args[0]은 숫자로 시작하더라도(예: 애플리케이션 데이터를 인용한 "-20004 rows rejected ...") 코드를 담지 않습니다. 1.8.0까지의 릴리스는 이런 앞쪽 숫자를 코드로 해석해 멀쩡한 연결을 무효화할 수 있었습니다(#608).

pycubrid는 args에 메시지만 담고 서버 오류 코드는 errno(및 code)에 담습니다. is_disconnect()는 알려진 문제 1에 나열된 서버 코드에 대해 errno를 읽습니다. pycubrid의 str()은 errno의 설명(예: -671의 Communication error. pycubrid 1.13.0은 -4를 Interrupted, SQLSTATE HY008로 설명하며, 이전 릴리스는 Communication error, 08S01로 보고했습니다)도 덧붙이므로, 메시지 패턴은 드라이버 자체 메시지인 args[0]과 비교합니다.


알려진 문제

1. 연결 해제 감지에 OperationalError를 사용하지 않음

CUBRIDdb 11.3.0.51은 OperationalError를 정의하지만(예외 계층 참고), 방언의 is_disconnect()는 예외 클래스로 연결 해제를 분류하지 않습니다. 대신: - 알려진 연결 해제 메시지 17종(공통 14종, pycubrid 3종)에 대한 문자열 패턴 매칭 - 끊겼거나 쓸 수 없는 연결의 CCI·CAS 코드에 대한 args[0](CUBRIDdb 전용) 숫자 오류 코드 매칭: -20004(CCI_ER_COMMUNICATION), -10003(CAS_ER_COMMUNICATION, CCI는 두 코드를 통신 오류로 취급), -20002(CCI_ER_CON_HANDLE), -20016(CCI_ER_CONNECT), -10002(CAS_ER_NO_MORE_MEMORY; CAS가 이 코드를 보낸 뒤 연결을 닫음). 1.8.0까지의 릴리스는 대신 -4, -10005, -10007, -21003, -21005를 나열했습니다(#572). -4는 서버의 ER_INTERRUPTED(KILL QUERY로 중단된 쿼리)여서 CUBRIDdb는 멀쩡한 연결을 무효화했고, -10005와 -10007은 CAS_ER_TRAN_TYPE과 CAS_ER_NUM_BIND, -21003과 -21005는 어느 Python 드라이버도 내지 않는 CUBRID JDBC 코드입니다. CUBRIDdb가 트랜잭션 중 CAS가 죽었을 때 내는 -20004와 -10002는 빠져 있었습니다(#578). - CAS의 cub_server 세션이 사라져 브로커가 CAS를 리셋하는 서버 오류에 대한 숫자 오류 코드 매칭(두 드라이버 모두): -111(ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED), -199(ER_NET_SERVER_CRASHED), -224(ER_OBJ_NO_CONNECT), -677(ER_BO_CONNECT_FAILED). cub_server가 중지되거나 비정상 종료되면 트랜잭션 중인 연결은 -111을 받고, 이후 트랜잭션이 끝날 때까지 서버가 다시 올라와도 모든 문장에서 -224를 받습니다(#565). -671(ER_CSS_RECV_OR_SEND)은 브로커가 CAS를 리셋하지 않으므로 포함하지 않습니다. CUBRID의 src/broker/cas_error.c는 release/11.4(3ea1d8d)와 release/10.2(ed716d0)에서 이 네 코드에만 CAS의 reset_flag를 설정하며, 다른 리셋 경로(세션 종료 시 롤백 실패, 커밋이나 중단 후 HA 페일백 상태, 재연결 시간)는 -671과 관계가 없습니다(#873). 문제 해결 참고. - pycubrid의 errno에 대해서만 하는 숫자 오류 코드 매칭으로, pycubrid가 실제로 받는 형태의 CAS 코드를 검사: pycubrid처럼 갱신된 오류 코드 프로토콜을 이해한다고 알리지 않는 드라이버에게 CUBRID의 CAS는 cas_error.h 코드에 9000을 더해 레거시 번호로 보냅니다. -1002(레거시 CAS_ER_NO_MORE_MEMORY, 즉 -10002 + 9000)가 이렇게 매칭됩니다(#578). - 번호가 겹치는 코드의 출처 판별(#916): 레거시 CAS 코드는 -1000~-1200으로 도착하며, 일부 서버 오류와 번호가 같습니다(CUBRID 11.4.6 ER_JSON_INVALID_PATH는 -1200). pycubrid 1.13은 오류를 보낸 쪽을 error_source("cas" 또는 "server")로 알려 줍니다. "server"이면 서버의 코드로 취급합니다: 서버의 -1002는 CAS_ER_NO_MORE_MEMORY가 아니고, 서버 텍스트는 애플리케이션 데이터를 담을 수 있으므로 메시지 패턴은 서버 접속 실패 코드(-190, -191, -368)에만 적용합니다. "cas", None, 또는 error_source가 없는 1.13 이전 pycubrid에서는 이 코드들을 이전과 같이 분류합니다.

2. CCI 라이브러리 의존성

드라이버는 CCI 라이브러리를 소스에서 컴파일해야 합니다. CI에서는 다음으로 처리:

git clone --branch v11.3.0.51 --depth 1 --recurse-submodules https://github.com/CUBRID/cubrid-python.git
cd cubrid-python/cci-src && mkdir build_x86_64_release && cd build_x86_64_release
cmake ../ && make -j$(nproc)

3. cursor.lastrowid 사용 불가

표준 DB-API의 cursor.lastrowid 속성이 구현되어 있지 않습니다. 방언은 connection.get_last_insert_id()를 대신 사용하며, SELECT LAST_INSERT_ID() SQL 폴백을 둡니다.

폴백은 활성 DBAPI 연결에서 일반 커서를 열고, ID를 가져온 뒤 커서를 닫습니다. 실행, fetch, 정수 변환에서 예외가 나도 닫습니다. 서버 측 커서 지원이 필요하지 않습니다. pycubrid 실행 컨텍스트도 cursor.lastrowid를 쓸 수 없을 때 같은 폴백을 사용합니다. 네이티브 드라이버 결과가 None이면 폴백 쿼리를 실행하지 않고 그대로 반환합니다.

4. CUBRID 12.x 호환성

CUBRID 12는 아직 출시되지 않았습니다. 출시되면 드라이버와 방언을 테스트하고 CI 매트릭스를 갱신할 예정입니다. 잠재 우려: - CCI API 변경 시 드라이버 재컴파일 필요 가능성 - 새 SQL 기능 시 컴파일러 갱신 필요 가능성 - 새 데이터 타입 시 타입 매핑 추가 필요 가능성

5. NUMERIC / DECIMAL 소수부 정밀도 손실

CUBRIDdb C-확장 드라이버는 NUMERIC / DECIMAL 값의 소수부를 DB-API 계층 아래에서 잘라냅니다: 15.7563을 저장한 컬럼이 15로 돌아옵니다. 드라이버 내부에서 SQLAlchemy가 값을 받기 전에 자릿수가 버려지므로, 어떤 방언 result processor도 이를 복원할 수 없습니다. 이는 방언 버그가 아니라 상류 드라이버의 한계입니다.

정확한 Decimal 라운드트립이 필요하면 순수 파이썬 cubrid+pycubrid:// 드라이버를 사용하세요 — 전체 정밀도로 Decimal 값을 네이티브 반환합니다(CUBRID 11.4에서 확인). 이것이 신규 프로젝트에 pycubrid를 권장하는 이유 중 하나입니다.

6. BLOB / CLOB 조회는 LOB 로케이터를 반환

CUBRID 10.2 및 11.4에서 Core/ORM, 동기/비동기 전 조합을 pycubrid 1.8.0(지원 최저 버전)과 pycubrid main 양쪽으로 실제 검증했습니다(#485; test/test_lob_value_contract.py, 재검증 시 다시 실행). 모든 릴리스된 드라이버에서 BLOB / CLOB 컬럼에 bytes / str을 바인딩하면 전체 값이 저장되고 NULL은 None으로 왕복됩니다. 그러나 NULL이 아닌 BLOB / CLOB 컬럼을 조회하면 bytes / str 대신 드라이버의 LOB 로케이터가 반환됩니다:

드라이버 URL NULL이 아닌 BLOB / CLOB 조회 결과
cubrid:// (CUBRIDdb 11.3) 서버 파일 로케이터 str ('file:...')
cubrid+pycubrid:// (pycubrid 1.8.0 및 main) LOB 핸들 dict (lob_type, lob_length, file_locator, ...)
cubrid+aiopycubrid:// (pycubrid 1.8.0 및 main) file_locator 문자열 (None을 포함한 LargeBinary / BLOB 값 바인딩은 #500부터 정상 동작)

LargeBinary / BLOB의 경우 SQLAlchemy 결과 프로세서가 TypeError를 발생시킵니다. 내용을 읽으려면 서버에서 변환(CLOB_TO_CHAR(col), BLOB_TO_BIT(col))하거나, 대용량 텍스트는 str로 왕복되는 sqlalchemy.Text(CUBRID STRING)에 저장하세요. pycubrid의 LOB 핸들(cubrid-lab/pycubrid#441/#442, 완료)은 pycubrid.compat.native의 명시적 호출(connection.lob(), cursor.fetch_lob(), cursor.bind_lob(), 동기 전용)입니다. 방언은 이 모듈을 사용하지 않으며 pycubrid main(9fbbc05에서 확인)의 일반 조회는 여전히 로케이터를 반환하므로, test_lob_value_contract.py는 릴리스된 드라이버와 pycubrid main 양쪽에서 동일하게 strict xfail 처리합니다 — pycubrid가 일반 조회 동작을 바꾸면 xfail이 실패하는 XPASS로 바뀌어 조용히 통과하지 않습니다. 방언은 서버 변환을 대신 수행하지 않습니다. 모든 BLOB / CLOB 조회를 BLOB_TO_BIT / CLOB_TO_CHAR로 감싸면 생성되는 SQL이 바뀌고, text() 문에는 적용되지 않으며, 값 전체를 메모리에 올리기 때문입니다. 타입도 참고하세요.

7. executemany가 None에 이전 행의 값을 재사용 (방언 가드)

CUBRIDdb 11.3.0.51(cubrid:// 드라이버)의 executemany()에는 두 가지 버그가 있습니다:

  • 잘못된 데이터. _bind_params가 None을 건너뛰고 executemany는 문장을 한 번만 준비(prepare)합니다. 따라서 None 파라미터에는 이전 행에서 바인딩된 값이 그대로 남아, [(1, 'a'), (2, None)]이 (2, 'a')로 저장됩니다.
  • 잘못된 rowcount. cursor.rowcount가 마지막 행의 개수만 보고합니다.

SQLAlchemy를 통하면 text() 및 Core UPDATE/DELETE executemany가 잘못된 데이터를 저장했고, ORM의 일괄 UPDATE는 StaleDataError(expected to update 3 row(s); 1 were matched)를 발생시켰습니다. bind_param(i, None)이 SystemError를 발생시키므로 드라이버 외부에서 우회할 수도 없습니다.

방언 가드(#502). CubridDialect.do_executemany는 각 파라미터 세트를 cursor.execute()로 실행하고 cursor.rowcount를 합계로 설정합니다. 각 execute()가 문장을 다시 준비하므로 바인딩되지 않은 None은 NULL이 됩니다. 마지막 행 rowcount는 모든 문장에서 틀리므로, 가드는 None이 있는 행에만이 아니라 항상 적용됩니다. 가드 덕분에 supports_sane_multi_rowcount는 두 드라이버 모두에서 정확합니다. 대가는 일반 executemany에서 행마다 한 번의 문장 준비입니다. insertmanyvalues를 사용하는 여러 행의 Core insert()는 do_execute를 거치므로 가드를 사용하지 않습니다. 타입이 bind_expression()을 정의하는 컬럼이 있는 테이블에 대한 INSERT는 executemany로 대체되므로(#421) cubrid://에서는 행 단위 가드를 사용합니다. SQLAlchemy 없이 CUBRIDdb의 cursor.executemany()를 직접 호출하는 코드는 여전히 영향을 받습니다.

cubrid+pycubrid://와 cubrid+aiopycubrid://는 None을 올바르게 바인딩하고 rowcount를 합산하므로 드라이버의 한 번만 준비하는 executemany를 그대로 사용합니다.

수정된 CUBRIDdb 릴리스가 최소 지원 버전이 되면 가드를 제거합니다. CUBRID/cubrid-python에 대한 상류 보고는 아직 제출 전이며 #502에서 추적합니다.

8. commit() / rollback() 이후 다 읽지 않은 결과

여러 번의 FETCH 왕복이 필요한 500행 결과로 CUBRID 10.2 및 11.4에서 실제로 검증했습니다(#481). pycubrid는 배치당 100행을 가져오며, 브로커의 첫 응답에는 1000바이트 행이 16개만 담겼습니다. Connection.commit() / rollback() 이후 동기 Result의 나머지를 읽을 때 동작은 드라이버마다 다릅니다:

드라이버 commit() 이후 rollback() 이후
cubrid:// (CUBRIDdb 11.3) 모든 행 InterfaceError (CCI -20040)
cubrid+pycubrid:// (pycubrid 1.7.1) 이전 쿼리를 완료한 연결(풀링된 연결의 일반적인 상태)에서는 이미 버퍼에 있는 행만 오류 없이 반환, 그 외에는 OperationalError commit과 동일
cubrid+aiopycubrid:// 모든 행: AsyncConnection.execute()가 반환 전에 전체 결과를 버퍼링 모든 행
원시 pycubrid.aio 커서 (pycubrid 1.7.1) 버퍼에 있는 행만 오류 없이 반환 commit과 동일

pycubrid 1.8.0에서 수정됨: cubrid-lab/pycubrid#395는 pycubrid가 부분 결과를 반환하는 대신 InterfaceError를 발생시키도록 하며, 계약 테스트는 이 동작을 요구합니다. pycubrid 1.7.1 이하에서는 트랜잭션을 끝내기 전에 결과를 모두 소비하세요. 방언이 서버 측 커서를 지원하지 않으므로 AsyncConnection.stream()은 사용할 수 없습니다.

9. pycubrid 1.7.1 이하의 NOT NULL / 외래 키 위반

CUBRID 10.2 및 11.4에서 실제로 검증했습니다(#480). SQLAlchemy는 전달받은 DB-API 예외 클래스를 그대로 감싸므로, SQLAlchemy 예외 클래스는 드라이버에 따라 달라집니다:

위반 (네이티브 코드) cubrid:// (CUBRIDdb 11.3) cubrid+pycubrid:// / cubrid+aiopycubrid:// (pycubrid 1.7.1)
NOT NULL (-631) IntegrityError DatabaseError
외래 키 (-922) IntegrityError DatabaseError
고유 / 기본 키 (-670) IntegrityError IntegrityError

pycubrid 1.8.0에서 수정됨: cubrid-lab/pycubrid#390으로 NOT NULL 및 외래 키 위반이 CUBRIDdb와 같이 IntegrityError로 발생하며, 계약 테스트는 이 동작을 요구합니다. pycubrid 1.7.1 이하에서는 pycubrid를 통한 NOT NULL 및 외래 키 실패를 sqlalchemy.exc.DatabaseError(IntegrityError의 기반 클래스)로 잡으세요. 방언은 의도적으로 메시지 기반으로 예외를 재분류하지 않습니다. 모든 드라이버에서 rollback() 후 연결이나 Session을 계속 사용할 수 있습니다.

pycubrid 1.8.x(지원 하한), pycubrid 1.9.0에서 수정됨 (CUBRID 10.2 및 11.4에서 검증, #479):

경우 pycubrid 1.8.x pycubrid >= 1.9.0
외래 키로 제한된 부모 DELETE/UPDATE (-924), 참조되는 테이블의 TRUNCATE (-1284) DatabaseError IntegrityError (cubrid-lab/pycubrid#493)
완전한 응답 안에 pycubrid가 표현할 수 없는 값, 예를 들어 0 DATE/DATETIME ('0000-00-00') 연결 해제로 보고되는 OperationalError("malformed response from broker"): 연결이 무효화됨 DataError (cubrid-lab/pycubrid#492, #512, #543): 같은 연결이 다음 문장을 실행함

계약 테스트는 1.9.0 동작을 요구하며, pycubrid 1.8.x에서는 엄격한 예상 실패(strict xfail)입니다.

10. pycubrid는 CAS 재시작 후 세션을 교체 (방언이 격리 수준을 다시 적용)

pycubrid 1.8.0에서 수정되었습니다 (cubrid-lab/pycubrid#468, #472). 1.8.0이 지원하는 최소 버전입니다. 1.8.0 이전에는 드라이버의 commit() / rollback()마다(이때 브로커는 CAS를 트랜잭션 밖으로 보고합니다) pycubrid가 새 브로커 세션을 열었기 때문에, 격리 수준이 서버 기본값(READ COMMITTED)으로 돌아가고 세션 변수가 사라졌습니다(#505). pycubrid 1.8.0은 commit() / rollback() 이후와 오토커밋 모드에서도 CAS 세션을 유지합니다. 다음 요청 전에 트랜잭션 밖의 CAS를 CHECK_CAS로 확인하고, CAS가 사라졌을 때만 재연결합니다. CUBRIDdb는 항상 같은 세션을 유지했습니다.

잔여 사례: 교체된 CAS. CAS가 실제로 사라지면(예: APPL_SERVER_MAX_SIZE 메모리 재시작처럼 트랜잭션 후의 CAS 재시작, 또는 브로커 리셋) pycubrid 1.8.0은 새 세션을 엽니다. 그 세션은 서버 기본 격리 수준으로 시작하고, pycubrid는 autocommit만 복원하며, 원시 SQL로 설정한 세션 상태(SET @var, 직접 실행한 SET TRANSACTION 문)는 사라집니다. 방언 없이도 이 경우에 격리 수준이 유지되어야 한다면 서버 쪽에서 설정하세요(cubrid.conf의 isolation_level).

방언의 다시 적용. cubrid+pycubrid://와 cubrid+aiopycubrid://는 각 연결에 설정한 격리 수준을 기억했다가, 수준을 설정한 연결에서만 commit과 rollback마다 다시 적용합니다(SET TRANSACTION ISOLATION LEVEL + COMMIT). 엔진 수준 isolation_level은 commit, rollback, 풀 반환 후에도 유지됩니다. 연결 수준 execution_options(isolation_level=...)는 해당 Connection이 열려 있는 동안 commit과 rollback 후에도 유지되며, 풀 반환 시에는 SQLAlchemy가 엔진 수준(설정이 없으면 서버 기본값)을 복원하고, 엔진 수준 설정이 없는 엔진에서는 풀 반환 시 해당 연결의 다시 적용이 중단됩니다. 다시 적용은 트랜잭션이 끝난 뒤의 첫 요청이므로, 그 시점에 CAS가 교체되면 새 세션에서 실행되어 격리 수준이 유지됩니다(CUBRID 11.4와 pycubrid 1.8.0에서 commit() 직후 CAS를 종료해 검증했습니다. 격리 수준은 SERIALIZABLE로 유지되었고, 다시 적용이 없으면 READ COMMITTED로 떨어졌습니다). 다시 적용 자체가 실패해도 commit이나 rollback은 성공으로 보고되고(rollback을 일으킨 원래 예외도 그대로 전달됨), 실패는 경고로 기록된 뒤 다음 트랜잭션 시작 시 재시도되며, 거기서 다시 실패하면 문장 실행 전에 예외가 발생합니다. 다시 적용이 성공하거나 그 연결에 새 격리 수준을 설정할 때까지 연결은 표시된 채로 남으므로, 이후의 모든 트랜잭션 시작이 다시 시도하고 다시 예외를 일으키며 확인되지 않은 격리 수준으로 실행되지 않습니다(#937). 앞서 성공한 commit은 다시 실행하지 않습니다.

수준을 설정한 유휴 연결. 다시 적용의 SQL COMMIT은 CAS를 트랜잭션 중으로 표시해 두므로, 브로커는 풀링된 연결이 유휴 상태인 동안에도 그 CAS를 연결에 묶어 둡니다(cubrid broker status에서 CLIENT_WAIT, 수준 설정이 없으면 CLOSE_WAIT). 풀 크기에 맞게 브로커의 MAX_NUM_APPL_SERVER를 잡으세요. 또한 이런 연결의 CAS가 유휴 중에 종료되면 pycubrid가 스스로 재연결하지 않습니다. 다음 문장은 OperationalError를 발생시키고, 방언은 이를 연결 끊김으로 보고하므로 SQLAlchemy는 서버 기본 수준으로 조용히 실행하는 대신 그 연결을 버립니다. create_engine(..., pool_pre_ping=True)를 사용하면 풀이 체크아웃 시 연결을 교체하고, 새 연결은 엔진 수준을 받습니다.

pycubrid의 AUTOCOMMIT. pycubrid 1.8.0은 오토커밋 모드에서도 세션을 유지하므로, CUBRIDdb와 마찬가지로 문장은 적용 중이던 서버 수준을 유지하고 세션 변수도 문장 사이에 유지됩니다. 연결을 AUTOCOMMIT으로 바꾸면 방언은 격리 수준 다시 적용을 멈춥니다.

11. 컬렉션 파라미터 (SET, MULTISET, SEQUENCE)

컬렉션 컬럼에 Python list, tuple, set을 바인딩하는 방식은 드라이버마다 다릅니다(#484, CUBRID 10.2와 11.4에서 검증).

드라이버 list / tuple / set 바인딩 컬렉션 조회
pycubrid >= 1.9.0 (타입 지정 컬렉션 파라미터, cubrid-lab/pycubrid#567) 방언이 값을 컬럼 타입에 맞는 pycubrid.types.Set, Multiset, Sequence로 감쌉니다. SET, MULTISET, SEQUENCE의 의미가 유지됩니다. SEQUENCE는 list나 tuple만 받으며, set/frozenset은 TypeError("SEQUENCE is ordered; pass a list or tuple")를 발생시킵니다. ?decode_collections=true이면 frozenset(SET) 또는 list(MULTISET, SEQUENCE), 없으면 원시 bytes
pycubrid 1.8.x (지원 최저 버전) ProgrammingError: pycubrid가 컬렉션 파라미터를 거부합니다. SEQUENCE에 넘긴 set/frozenset은 방언이 위와 같은 TypeError를 발생시킵니다. 위와 같음
CUBRIDdb 11.3.0.51 CUBRIDdb가 값을 직접, 항상 SET 호스트 변수로 바인딩합니다. MULTISET은 중복을, SEQUENCE는 순서를 잃습니다([3, 1, 2, 1]은 {1, 2, 3}으로 저장됨). None 원소는 지원되지 않습니다. [None]은 드라이버 안에서 UnboundLocalError로, [1, None]은 -494 Cannot coerce host var to type sequence로 실패합니다. 원소 타입과 관계없이 str 원소로 된 set(SET) 또는 list(MULTISET, SEQUENCE)

방언은 CUBRIDdb의 동작을 바꾸지 않습니다. CUBRIDdb에서 중복이나 순서가 필요한 컬렉션은 바인드 파라미터로 보내지 말고 SQL에 컬렉션 리터럴을 쓰거나(MULTISET{1, 1}, SEQUENCE{3, 1, 2}) cubrid+pycubrid://를 사용하세요. 타입 매핑, 컬렉션 값을 참고하세요.

12. 일반 스칼라 실행은 pycubrid의 compat.native prepared 코어를 사용하지 않음

pycubrid 1.8.0(지원 최저 버전)은 pycubrid.compat.native 아래에 추가적이고 선택적인 동기 prepared/typed 스칼라 바인딩 커서를 추가했습니다(INT32, UTF-8 CHAR, SQL NULL만 지원; cubrid-lab/pycubrid#439). cubrid+pycubrid://도 cubrid+aiopycubrid://도 이를 import하거나 호출하지 않습니다: SQLAlchemy는 모든 스칼라 바인딩(정수, UTF-8/CJK 문자열, NULL, 반복 실행, executemany, Core와 ORM)에 대해 테스트한 모든 드라이버에서 계속 일반 PEP 249 execute() / executemany() 경로(FC41)를 사용하며, 동작 차이나 방언 변경이 없습니다(#483; test/test_scalar_execution_contract.py는 방언 소스가 pycubrid.compat를 참조하지 않는지, 그리고 일반적인 사용에서 pycubrid.compat.native.connect()가 절대 호출되지 않는지도 검증합니다). typed prepared 실행을 사용하는 SQLAlchemy/DB-API 통합은 설치된 드라이버에 compat.native가 존재한다고 해서 자동으로 생기는 것이 아니라, 별도로 설계되고 릴리스되어야 하는 기능입니다.

13. CUBRIDdb는 값을 서버에서 바인딩

pycubrid는 바인딩한 값을 SQL 문자열에 써 넣은 뒤 보내고, CUBRIDdb는 ? 표식이 있는 문장을 보낸 뒤 서버에서 값을 바인딩합니다. 그래서 다음 두 결과가 다릅니다(#1003, CUBRID 10.2와 11.4에서 CUBRIDdb 11.3.0.51과 pycubrid 1.13.1로 검증).

  • THEN/ELSE 값이 모두 바인드 파라미터인 CASE는 CUBRIDdb에서 문자열을 반환합니다. select(case({True: 10}, value=..., else_=20))와 검색형 case((cond, 10), else_=20)는 '10', '20'을 반환하고, pycubrid에서는 10, 20을 반환합니다. 서버가 이런 CASE의 타입을 문자열로 정하기 때문입니다. cast(..., Integer)로 감싸거나(CAST(CASE ... END AS INTEGER)는 두 드라이버 모두에서 10을 반환) 값에 literal_column("10")을 쓰십시오.
  • 바인드 파라미터가 들어 있는 GROUP_CONCAT 인자와 aggregate_order_by() 항목은 CUBRIDdb에서 실패합니다(-494). CUBRID는 ORDER BY 항목이 인자와 같은 식이어야 하는데, CUBRIDdb는 func.group_concat(and_(t.c.a == 1, t.c.b == 2)).aggregate_order_by(...)의 두 사본을 각자의 표식으로 보내므로 서버가 같은 식으로 보지 않습니다. pycubrid는 같은 리터럴 SQL을 두 번 보내므로 성공합니다. CUBRIDdb에서는 인자 위치로 정렬하거나(aggregate_order_by(literal_column("1"))) 상수를 literal_column()으로 쓰십시오. 컬럼처럼 바인드 파라미터가 없는 인자는 두 드라이버 모두에서 동작합니다.

방언은 두 결과 모두 바꾸지 않습니다.


설치 참고

순수 Python pycubrid 방언 변형은 pycubrid>=1.8.0,<2.0과 함께 sqlalchemy-cubrid[pycubrid]를 설치하세요. pycubrid 1.8.0이 최소 버전인 이유는 commit() / rollback() 이후에도 CAS 세션을 유지하고, 계약 테스트가 요구하는 수정을 포함하기 때문입니다(알려진 문제 8~10). pool_pre_ping이 사용하는 네이티브 동기·비동기 ping(False)도 제공합니다.

[pycubrid] extra는 동기·비동기 연결을 모두 지원하며, SQLAlchemy 2.0과 2.1에서 greenlet을 제공하는 SQLAlchemy[asyncio]를 포함합니다. 기여자용 test 의존성 그룹(dev에 포함)도 비동기 테스트 import를 위해 이 브리지를 포함합니다. 기본 설치의 SQLAlchemy 의존성은 유지됩니다. pycubrid 드라이버 자체는 순수 Python이지만, 호환 wheel이 없으면 greenlet 설치에 빌드 도구가 필요할 수 있습니다.

PyPI CUBRID-Python 9.3.x는 지원하지 않음

PyPI의 최신 CUBRID-Python 릴리스는 9.3.0.2입니다(sdist만 있으며 2015년 업로드). PyPI에는 11.x 릴리스가 없습니다. [cubrid]와 [cubriddb] extra는 버전 제한 없이 CUBRID-Python에 의존하므로 9.3.x를 설치합니다. 이 드라이버는 이 방언과 함께 테스트되지 않았습니다. CUBRID 11.4에서 BIGINT 값(COUNT(*) 포함)을 str로 반환하고(#583), 통합 테스트 일부가 실패합니다: autocommit, 대용량 객체 왕복, fetch 결과 형태, ping, 재귀 CTE(#585).

  • [cubrid]와 [cubriddb] extra는 폐기 예정(deprecated)입니다. 기존 설치가 계속 해석되도록 유지하지만, 지원되는 드라이버를 설치할 수는 없습니다.
  • cubrid:// 또는 cubrid+cubriddb:// 엔진은 첫 연결 시 로드된 드라이버의 버전 (_cubrid.__version__)을 읽고, 11.3보다 오래되었으면 sqlalchemy.exc.SAWarning을 냅니다. 기존 배포가 계속 동작하도록 연결을 거부하지 않고 경고만 합니다. 즉시 실패하게 하려면 경고를 오류로 바꾸세요: warnings.filterwarnings("error", message="CUBRIDdb .* is older", category=SAWarning).
  • 지원되는 방법: 권장 순수 Python 드라이버(pip install "sqlalchemy-cubrid[pycubrid]", cubrid+pycubrid://)를 사용하거나, 아래처럼 CUBRIDdb를 소스에서 빌드하세요.

소스에서 CUBRIDdb 빌드

CI가 사용하는 절차입니다:

# CCI 서브모듈과 함께 드라이버 클론
git clone --branch v11.3.0.51 --depth 1 --recurse-submodules \
  https://github.com/CUBRID/cubrid-python.git
cd cubrid-python

# CCI 라이브러리 빌드
mkdir -p cci-src/build_x86_64_release && cd cci-src/build_x86_64_release
cmake ../ && make -j$(nproc)
cd ../..

# setup.py의 CCI 재빌드를 건너뛰고 설치
printf '#!/bin/bash\nexit 0\n' > build_cci.sh
pip install .

소스 빌드는 cubrid_python 배포판을 설치합니다. 버전의 네 번째 자리는 릴리스 태그가 아니라 git 커밋 수이므로, v11.3.0.51의 얕은 클론은 11.3.0.0001로 표시됩니다.

설치 확인

import CUBRIDdb
print(CUBRIDdb._cubrid.__version__)  # v11.3.0.51 빌드는 b'11.3.0.0001', PyPI 릴리스는 b'9.3.0.0001'

참고: 연결 가이드 · 개발 가이드 · 기능 지원