파라미터 바인딩 (한국어)¶
🌐 PARAMETER_BINDING.md의 번역입니다. 영어 원문이 표준이며, 페이지 번역은 경고 수준의 동기화 규칙을 따릅니다.
pycubrid 1.x의 드라이버 측 파라미터 바인딩 계약.
이 문서는 pycubrid가 Python 값을 SQL 리터럴로 변환해 CUBRID로 보내는 방식의 권위 있는 명세입니다. 이 문서의 모든 주장은 구현(pycubrid/_cursor_common.py)과 동작을 고정하는 단위 테스트에 대한 인용으로 뒷받침됩니다. 아래 규칙의 모든 변경은 RELEASE_POLICY.md가 다루는 계약 변경입니다.
목차¶
- 개요
- 플레이스홀더 방식
- 타입 매핑 (보장)
- 문자열 이스케이프
- 이스케이프 모드 협상
- 리터럴 모드
- 이스케이프 처리 모드
- 플레이스홀더 토크나이저
executemany- 비보장과 명시적 한계
- 호환성 정책 (1.x)
- 고정 테스트
- 참고 자료
개요¶
pycubrid는 드라이버 측 리터럴 바인딩을 수행합니다. cursor.execute(sql, parameters)를 호출하면 드라이버는:
sql을 따옴표 밖·주석 밖의?플레이스홀더를 기준으로 세그먼트 분할(split_on_placeholders,pycubrid/_cursor_common.py:46-119).- 플레이스홀더 수가
len(parameters)와 일치하는지 검증(pycubrid/_cursor_common.py:199-203). - 각 Python 값을
format_parameter로 SQL 리터럴 문자열로 변환(pycubrid/_cursor_common.py:141-181). - 세그먼트와 렌더링된 리터럴을 하나의 SQL 문자열로 연결(
pycubrid/_cursor_common.py:204-208). - 완전히 렌더링된 SQL을
PrepareAndExecutePacket으로 CUBRID에 전송(pycubrid/cursor.py:139-150,pycubrid/aio/cursor.py:114-117).
바인딩 구현은 CursorParamsMixin을 통해 동기(Cursor)와 비동기(AsyncCursor) 경로에서 그대로 공유됩니다(pycubrid/_cursor_common.py:237-257). 동기·비동기 바인딩에 동작 차이가 없으며, 동등성은 tests/test_aio_cursor_parity.py와 tests/test_split_placeholders.py로 강제됩니다.
이것은 서버 측 prepared-statement 바인딩이 아닙니다. pycubrid는 파라미터 값을 별도의 타입 페이로드로 보내지 않으며, 브로커는 execute마다 완전한 SQL 텍스트를 받습니다. 비보장과 명시적 한계를 참고하세요.
플레이스홀더 방식¶
paramstyle = "qmark"(pycubrid/__init__.py:46), PEP 249 준수.- 플레이스홀더는 위치 기반
?입니다. named·numeric·pyformat 플레이스홀더는 없습니다. execute()의parameters인자는str/bytes/bytearray가 아닌Sequence여야 합니다. 매핑은ProgrammingError로 거부됩니다(pycubrid/_cursor_common.py:194-197). 정확한 메시지 문구는 참고 사항이며 비보장과 명시적 한계를 참고하세요.- 플레이스홀더 수 불일치는
ProgrammingError를 발생시킵니다(pycubrid/_cursor_common.py:199-203). 정확한 메시지 문구는 참고 사항입니다.
타입 매핑 (보장)¶
다음 표는 1.x의 권위 있는 타입→리터럴 매핑입니다. 모든 행은 pycubrid/_cursor_common.py의 구현 위치와 동작을 고정하는 테스트를 인용합니다.
각 오류 사례에서 발생하는 예외 클래스(예:
ProgrammingError)는 계약의 일부이며, 표에 보이는 메시지 문구는 예시일 뿐이며 1.x 내에서 다듬어질 수 있습니다. 비보장과 명시적 한계를 참고하세요.
| Python 타입 | SQL 리터럴 | 구현 | 고정 테스트 |
|---|---|---|---|
None |
NULL |
_cursor_common.py:143-144 |
tests/test_param_security.py:95-97 |
bool |
1 (True) / 0 (False) |
_cursor_common.py:145-146 |
tests/test_param_security.py:98-102 |
int |
str(value) (10진수) |
_cursor_common.py:177-180 |
tests/test_param_security.py:107-109 |
float |
str(value); nan/inf/-inf는 ProgrammingError 발생 (현재 메시지: "nan and inf are not supported by CUBRID") |
_cursor_common.py:177-180 |
tests/test_param_security.py:132-142 |
decimal.Decimal |
str(value) (따옴표 없음) |
_cursor_common.py:175-176 |
tests/test_param_security.py:113-115 |
str |
작은따옴표 리터럴; 문자열 이스케이프 적용; NUL(U+0000)과 Ctrl-Z(U+001A, \x1a)는 각각 ProgrammingError 발생 (현재 메시지: "string parameter contains null byte", "string parameter contains Ctrl-Z (0x1A) byte") |
_cursor_common.py:147-148, 124-138 |
tests/test_param_security.py:27-84 |
bytes, bytearray |
X'<hex>' (소문자 hex) |
_cursor_common.py:149-150 |
tests/test_param_security.py:104-106, 144-145 |
datetime.datetime (naive) |
DATETIME'YYYY-MM-DD HH:MM:SS.mmm' — 마이크로초는 밀리초로 절사(value.microsecond // 1000) |
_cursor_common.py:151-152, 170 |
tests/test_param_security.py:124-127 |
datetime.datetime (tz 포함) |
DATETIMETZ'YYYY-MM-DD HH:MM:SS.mmm <tz>' — <tz>는 tzinfo.key가 있으면 그 값(예: Asia/Seoul), 없으면 ±HH:MM 숫자 오프셋 |
_cursor_common.py:151-169 |
tests/test_param_security.py:147-169 |
datetime.date |
DATE'YYYY-MM-DD' |
_cursor_common.py:171-172 |
tests/test_param_security.py:116-118 |
datetime.time |
TIME'HH:MM:SS' — 마이크로초 버림 |
_cursor_common.py:173-174 |
tests/test_param_security.py:120-122 |
| 그 외 전부 | ProgrammingError (현재 메시지: "unsupported parameter type") |
_cursor_common.py:181 |
tests/test_param_security.py:128-130; tests/test_cursor.py:233-235 |
바인딩 값으로 명시적으로 미지원¶
datetime.timedelta— 분기가 없음;ProgrammingError("unsupported parameter type")발생.pycubrid.Lob—Lob인스턴스는 SQL 리터럴로 변환되지 않습니다.BLOB/BIT타입 컬럼에는 rawbytes를 삽입하고, 대형 객체 워크플로우에는Connection.create_lob()과 LOB 쓰기 API를 사용하세요(pycubrid/lob.py,pycubrid/connection.py:333-339).- 컬렉션(
list,tuple,set,frozenset,dict)을 단일 바인딩 값으로 — 실행 가능한 메시지와 함께ProgrammingError발생 (현재 문구:cannot bind a collection (list/tuple/set/frozenset/dict) as a single parameter; pycubrid does not auto-expand IN (?, ?, ...) — expand the placeholders explicitly in the SQL). 자동IN (?, ?, ?)확장은 없습니다. SQL에 플레이스홀더를 명시적으로 펼치세요. - 임의의 Python 객체 —
ProgrammingError("unsupported parameter type")발생.
문자열 이스케이프¶
문자열 이스케이프는 escape_string(pycubrid/_cursor_common.py)이 수행합니다. 동작은 no_backslash_escapes 연결 플래그(pycubrid/_connection_common.py)에 따라 달라집니다. 기본적으로 이 플래그는 연결 시점에 라이브 서버에서 자동 협상됩니다(이스케이프 모드 협상 참고). 감지를 덮어쓰려면 pycubrid.connect(..., no_backslash_escapes=True|False)로 명시적으로 전달하세요.
모든 모드에서:
- 리터럴은 작은따옴표로 감싸집니다.
- 입력의 NUL(
U+0000)은ProgrammingError발생(pycubrid/_cursor_common.py:130-131). 이것은 무조건적이며 양쪽 모드에 적용됩니다. 정확한 메시지 문구는 참고 사항입니다. - 작은따옴표는 doubling됩니다(
'→''). - 유니코드 코드 포인트(UTF-16이 서로게이트 페어로 인코딩할 비-BMP 문자 포함)는 변경 없이 통과합니다(
tests/test_param_security.py::TestEscapeString::test_unicode_passthrough,::test_unicode_non_bmp_passthrough).
이스케이프 모드 협상¶
CUBRID의 no_backslash_escapes 시스템 파라미터 기본값은 yes입니다 — 백슬래시는 이스케이프 마커가 아니라 일반 리터럴 문자입니다(CUBRID 매뉴얼, literal.rst: "백슬래시 이스케이프를 사용하려면 cubrid.conf의 no_backslash_escapes를 no로 설정해야 합니다. 하지만 기본값은 yes입니다."). 드라이버가 그런 서버에 대해 맹목적으로 백슬래시를 doubling하면 C:\temp\file이 C:\\temp\\file로 저장됩니다 — 조용한 데이터 오염입니다 (이슈 #255).
서버의 실제 설정이 무엇이든 정확을 유지하기 위해, no_backslash_escapes가 connect()에 전달되지 않으면 드라이버는 연결 시점에 SELECT CHAR_LENGTH('\\')(SQL 리터럴 '\\', 백슬래시 두 문자)로 라이브 서버를 한 번 프로브합니다:
- 결과
2→ 서버가 두 백슬래시를 그대로 둠 → 리터럴 모드, 드라이버는no_backslash_escapes=True로 고정(백슬래시를 doubling하지 않음). - 결과
1→ 서버가 쌍을 언이스케이프함 → 이스케이프 처리 모드, 드라이버는no_backslash_escapes=False로 고정. - 그 외 값이나 프로브 오류 →
OperationalError발생. 드라이버는 이스케이프 모드를 추측하지 않습니다. 잘못된 값은 문자열 이스케이프를 조용히 오염시키고(SQL 인젝션도 가능) 때문입니다. 프로브를 실행할 수 없을 때는no_backslash_escapes를 명시적으로 전달해 감지를 건너뛰세요.
no_backslash_escapes=True 또는 False를 명시적으로 전달하면 프로브를 완전히 건너뜁니다. 협상은 물리적 연결당 한 번 일어나며 투명한 재연결에서 보존됩니다.
리터럴 모드 (no_backslash_escapes=True)¶
자동 협상이 기본 CUBRID 서버(no_backslash_escapes=yes)에서 선택하는 모드입니다:
- 작은따옴표 doubling(
'→''). - 백슬래시와 제어 문자는 그대로 둡니다(서버가 일반 문자로 취급하므로 바이트 단위로 왕복합니다).
- NUL 거부는 여전히 적용됩니다.
- 결과를 작은따옴표로 감쌉니다.
tests/test_aio_cursor_parity.py:99-105, tests/test_backslash_negotiation.py, 라이브 왕복 스위트 tests/test_integration.py::TestBackslashRoundTrip로 고정됩니다.
이스케이프 처리 모드 (no_backslash_escapes=False)¶
서버가 no_backslash_escapes=no로 실행 중이거나 명시적으로 고정했을 때 선택됩니다. 드라이버가 백슬래시를 doubling해서 서버가 다시 언이스케이프하게 합니다:
- 백슬래시 doubling(
\→\\). - 작은따옴표 doubling(
'→''). \r과\n은 각각 백슬래시 접두(\n→\\n등).\x1a(Ctrl-Z)는 안전한 CUBRID 리터럴 이스케이프가 없어 양쪽 이스케이프 모드에서ProgrammingError발생(문자열 이스케이프 참고).- 결과를 작은따옴표로 감쌉니다.
tests/test_param_security.py:27-55와 tests/test_aio_cursor_parity.py:87-96로 고정됩니다.
플레이스홀더 토크나이저¶
split_on_placeholders(pycubrid/_cursor_common.py:46-119)는 SQL 텍스트를 파싱하고 실행 가능한 SQL에 나타나는 ? 문자만을 기준으로 세그먼트를 분할합니다 — 문자열 리터럴·식별자 따옴표·주석 내부는 절대 아닙니다. 구체적으로 토크나이저는 다음을 인식하고 건너뜁니다:
- 작은따옴표 문자열 리터럴(
'...'), doubling된 따옴표 이스케이프('') 포함. - 큰따옴표 식별자(
"..."), doubling된 따옴표 이스케이프("") 포함. - 행 주석(
-- ... <EOL>). - 블록 주석(
/* ... */).
위의 어느 것이든 내부의 ?는 SQL 텍스트의 일부이며 플레이스홀더로 취급되지 않습니다. 이것은 tests/test_split_placeholders.py 전체로 고정됩니다.
치환 단계는 세그먼트와 렌더링된 리터럴을 연결합니다(pycubrid/_cursor_common.py:204-208); 순진한 str.replace("?", ...)를 수행하지 않으므로, 한 파라미터의 렌더링된 값에 리터럴 ?가 있어도 다음 플레이스홀더를 실수로 소비하지 않습니다.
executemany¶
DML 동사(INSERT, UPDATE, DELETE, MERGE)에 대해 executemany는:
- 파라미터 행마다
_bind_parameters(sql, params)를 한 번 호출해 행마다 완전히 렌더링된 SQL 문자열을 만듭니다. - 렌더링된 SQL 문자열 리스트를 하나의
BatchExecutePacket으로 보냅니다(pycubrid/cursor.py:252-257,pycubrid/aio/cursor.py:206-211),executemany에서executemany_batch로 디스패치(pycubrid/cursor.py:217,pycubrid/aio/cursor.py:191).
비-DML 문장에서는 executemany가 행별 execute 루프로 폴백합니다(pycubrid/cursor.py:220-238, pycubrid/aio/cursor.py:176-187).
각 행은 위의 동일한 타입 매핑 규칙으로 독립적으로 바인딩됩니다.
비보장과 명시적 한계¶
다음 동작들은 명시적으로 계약 밖이며 메이저 버전 없이 변경될 수 있습니다. 호출자가 암시적으로 의존하지 않도록 나열합니다.
- 서버 측 prepared-statement 바인딩 없음. 드라이버는 클라이언트에서 파라미터를 SQL 텍스트로 렌더링합니다. CUBRID는
execute마다 완전한 SQL 문자열을 받습니다. 별도의 타입 파라미터 페이로드도 클라이언트 측 문장 핸들 캐시도 없습니다. 성능 특성, 쿼리 플랜 캐싱, 로그 출력이 이 설계를 반영합니다. - 식별자는 이스케이프되지 않음. 식별자를 보간하는 코드 경로(특히
Cursor.callproc,pycubrid/cursor.py:328-336)는 따옴표 없이 식별자를 SQL 텍스트에 박습니다. 애플리케이션은 신뢰할 수 없는 입력에서 받은 식별자를 검증해야 합니다. 파라미터 바인딩(?)은 값에만 적용되며 식별자에는 절대 적용되지 않습니다. - 바인딩 계층의 타입 객체 동일성. PEP 249 타입 객체(
STRING,BINARY등)는cursor.description을 기술하며 파라미터 바인딩 중 참조되지 않습니다. - 서버 측 타입 강제 변환의 정규화 없음. 드라이버는 SQL 리터럴을 렌더링하고, CUBRID가 자기 규칙에 따라 리터럴을 대상 컬럼 타입으로 강제 변환합니다. 드라이버는 대상 컬럼에 맞춰 정밀도·스케일·문자셋을 조정하지 않습니다.
- 드라이버 강제 길이 제한 없음. 문자열, 바이트 버퍼, 렌더링된 SQL은 Python 메모리와 CUBRID 서버 제한으로만 제한됩니다.
- 자동
IN절 확장 없음. 단일 바인딩 값으로 전달된list/tuple/set은ProgrammingError를 발생시킵니다. SQL 텍스트에서 플레이스홀더를 직접 펼치세요 (예:f"... WHERE id IN ({','.join('?' * len(ids))})"에params=tuple(ids)). - 예외 메시지.
ProgrammingError메시지의 정확한 문구("unsupported parameter type", "wrong number of parameters", "string parameter contains null byte", "nan and inf are not supported by CUBRID", "parameters must be a sequence")는 계약의 일부가 아닙니다. 예외 클래스는 계약이며, 메시지 문구는 다듬어질 수 있습니다. - 위에 나열되지 않은 동작적 미묘함. 타입 매핑이나 문자열 이스케이프에 열거되지 않은 것은 보장이 아닙니다. 공개 API 서피스 게이트(
scripts/check_public_api.py)는 동작 드리프트를 감지하지 않습니다. 구조적 서피스 변경만 감지합니다.RELEASE_POLICY.md§"게이트가 감지하지 않는 것"을 참고하세요.
호환성 정책 (1.x)¶
1.x 라인 내에서 다음 변경들은 RELEASE_POLICY.md의 관리를 받습니다.
마이너(1.y → 1.y+1) 릴리스에서 허용¶
- 새 Python 타입 지원 추가(예:
uuid.UUID,datetime.timedelta) — 추가 전용이며 타입 매핑 표에 새 행이 추가됨. - 예외 클래스를 유지하면서 예외 메시지 개선.
- 이미 위에서 다루는 입력에 대해 생성되는 SQL 리터럴이나 발생하는 예외 클래스를 바꾸지 않는
escape_string,format_parameter,split_on_placeholders의 내부 리팩터링. - 바인딩 동작을 수정하는 새 연결 플래그 추가 — 단, 기본값이 이 문서의 규칙을 보존할 때만.
메이저(1.y → 2.0) 릴리스 필요¶
- 타입 매핑 표의 행 제거 또는 이름 변경.
- 표에 이미 있는 입력에 대한 SQL 리터럴 변경(예:
bytes의X'<hex>'를 다른 표현으로, 기본datetime정밀도 변경, tz 접미사 형식 변경). - NUL / NaN / Inf 거부의 강화 또는 완화.
no_backslash_escapes기본값 변경.paramstyle을"qmark"에서 변경.Mapping파라미터 거부 변경(즉, named-parameter 지원의 재도입 또는 제거).
지원 중단 흐름¶
미래의 메이저 릴리스에서 제거될 동작은 먼저 CHANGELOG.md > ### Deprecated에 deprecated로 표시되고, 문서화된 대체 수단과 함께 메이저 버전 제거 전 최소 한 개 마이너 릴리스의 마이그레이션 기간이 주어집니다.
고정 테스트¶
다음 테스트 모듈이 위에 문서화된 동작을 고정합니다. 이 중 어느 것이든 CI 실패는 계약 회귀입니다:
tests/test_param_security.py— 타입별 포매팅, NUL 거부, NaN/Inf 거부, datetime tz 렌더링, Decimal 처리, bytes hex 렌더링.tests/test_split_placeholders.py— 따옴표 문자열, 따옴표 식별자, 행 주석, 블록 주석 전반의 플레이스홀더 토크나이저 동작과_bind_parameters통합.tests/test_cursor.py(183-235행) — 다중 타입 파라미터 시퀀스의 종단 간 바인딩, 플레이스홀더 수 검증,Mapping/str파라미터 거부.tests/test_aio_cursor_parity.py(76-105행) — NUL 거부, 기본 이스케이프,no_backslash_escapes모드의 동기/비동기 동등성.
이 테스트들이 이 계약의 실행 가능한 명세를 제공합니다.
참고 자료¶
- 구현:
pycubrid/_cursor_common.py - 동기 진입점:
pycubrid/cursor.py - 비동기 진입점:
pycubrid/aio/cursor.py - 연결 플래그:
pycubrid/_connection_common.py - 릴리스 정책:
RELEASE_POLICY.md - 타입 시스템 (fetch / description 측): TYPES.md
- API 참조: API_REFERENCE.md