Skip to content

파라미터 바인딩 (한국어)

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

pycubrid 1.x의 드라이버 측 파라미터 바인딩 계약.

이 문서는 pycubrid가 Python 값을 SQL 리터럴로 변환해 CUBRID로 보내는 방식의 권위 있는 명세입니다. 이 문서의 모든 주장은 구현(pycubrid/_cursor_common.py)과 동작을 고정하는 단위 테스트에 대한 인용으로 뒷받침됩니다. 아래 규칙의 모든 변경은 RELEASE_POLICY.md가 다루는 계약 변경입니다.


목차


개요

pycubrid는 드라이버 측 리터럴 바인딩을 수행합니다. cursor.execute(sql, parameters)를 호출하면 드라이버는:

  1. sql을 따옴표 밖·주석 밖의 ? 플레이스홀더를 기준으로 세그먼트 분할(split_on_placeholders, pycubrid/_cursor_common.py:46-119).
  2. 플레이스홀더 수가 len(parameters)와 일치하는지 검증(pycubrid/_cursor_common.py:199-203).
  3. 각 Python 값을 format_parameter로 SQL 리터럴 문자열로 변환(pycubrid/_cursor_common.py:141-181).
  4. 세그먼트와 렌더링된 리터럴을 하나의 SQL 문자열로 연결(pycubrid/_cursor_common.py:204-208).
  5. 완전히 렌더링된 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 텍스트를 받습니다. 기존 커서의 유일한 예외는 옵트인 executemany(..., prepared=True)(#839)로, PREPARE + EXECUTE_ARRAY로 타입이 있는 호스트 변수를 보냅니다. 비보장과 명시적 한계를 참고하세요. 별도의 명시적 동기 pycubrid.compat.native prepared 커서는 타입 지정 값(INT, BIGINT, DOUBLE, NUMERIC, DATE, TIME, DATETIME, DATETIMETZ, VARBIT, 문자열, SQL NULL)을 FC2/FC3 타입 페이로드로 전송합니다. Prepared 호스트 변수 매핑을 참고하세요. 이 커서는 이 일반 1.x 계약을 대체하지 않습니다. 제한된 범위는 #418 타입 지정 CAS 설계와 API 참조를 참고하세요.


플레이스홀더 방식

  • 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 내에서 다듬어질 수 있습니다. 비보장과 명시적 한계를 참고하세요.

표의 날짜/시간 하위 클래스는 활성 C _datetime 구현이 있어야 합니다. 아래 대체 구현 제한을 참고하세요.

Python 타입 SQL 리터럴 구현 고정 테스트
None NULL _cursor_common.format_parameter tests/test_param_security.py:95-97
bool 1 (True) / 0 (False) _cursor_common.format_parameter tests/test_param_security.py:98-102
int (IntEnum/IntFlag 등 하위 클래스 포함) int.__repr__(value) (값의 10진수). 숫자 하위 클래스 참고 _cursor_common.format_parameter tests/test_param_security.py::TestFormatParameterTypes::test_int, ::test_numeric_subclass_renders_by_value
float (하위 클래스 포함) float.__repr__(value) (일반 float의 str()과 같은 최단 왕복 표기, 예: 1e+20). 단, -0.0은 -0.0e0으로, 비정규 값(0이 아니면서 크기가 sys.float_info.min보다 작은 값)은 두 정규 double의 정확한 곱 (m * 2.409919865102884e-181)으로 보냅니다(#988); nan/inf/-inf는 ProgrammingError 발생 (현재 메시지: "nan and inf are not supported by CUBRID") _cursor_common.format_parameter tests/test_param_security.py::TestFormatParameterTypes::test_float*, ::test_numeric_subclass_renders_by_value; tests/test_integration_float_literal.py
decimal.Decimal (하위 클래스 포함) 고정소수점 숫자(일반 Decimal로 변환한 값에 format(value, "f"), 따옴표 없음, E 표기 사용 안 함); 부호·후행 0·scale 유지; 리터럴 자릿수가 38을 넘으면 DataError; NaN/Infinity는 ProgrammingError 발생 (현재 메시지: "nan and inf are not supported by CUBRID"); C decimal 모듈이 없으면 하위 클래스는 ProgrammingError 발생. Decimal 파라미터와 숫자 하위 클래스 참고 _cursor_common.format_parameter tests/test_param_security.py::TestFormatParameterTypes::test_decimal*, ::TestPureDecimalFallback; tests/test_parity_integration.py::TestParityDecimalLiterals
str (하위 클래스 포함) 작은따옴표 리터럴; 값을 일반 str로 복사한 뒤 문자열 이스케이프 적용; NUL(U+0000)과 Ctrl-Z(U+001A, \x1a)는 각각 ProgrammingError 발생 (현재 메시지: "string parameter contains null byte", "string parameter contains Ctrl-Z (0x1A) byte"). 텍스트, 바이너리, 날짜/시간 하위 클래스 참고 _cursor_common.format_parameter / _cursor_common.escape_string tests/test_param_security.py:27-84, ::TestStrSubclassEscaping
bytes, bytearray (하위 클래스 포함) X'<hex>' (소문자 hex, bytes.hex(value) / bytearray.hex(value)) _cursor_common.format_parameter tests/test_param_security.py:104-106, 144-145, ::TestBinarySubclassRendering
datetime.datetime (naive, 하위 클래스 포함) DATETIME'YYYY-MM-DD HH:MM:SS.mmm' — 연도는 4자리로 0 채움; 마이크로초는 밀리초로 절사(microsecond // 1000) _cursor_common.format_parameter tests/test_param_security.py:124-127, ::TestTemporalSubclassRendering
datetime.datetime (tz 포함, 하위 클래스 포함) DATETIMETZ'YYYY-MM-DD HH:MM:SS.mmm <tz>' — <tz>는 tzinfo.key가 있으면 그 값(예: Asia/Seoul; 반복되는 DST 시각의 fold=1에서는 약어가 덧붙거나 오프셋으로 바뀜, 아래 참고), 없으면 ±HH:MM 숫자 오프셋. 비어 있지 않은 key는 [A-Za-z0-9_+/-]+에 맞는 일반 str이어야 하며, 아니면 ProgrammingError 발생 (현재 메시지: "time zone key must be an IANA name matching [A-Za-z0-9_+/-]+") _cursor_common.format_parameter / _cursor_common._format_tz tests/test_param_security.py:147-169, ::TestTzinfoKey
datetime.date (하위 클래스 포함) DATE'YYYY-MM-DD' — 연도는 4자리로 0 채움 _cursor_common.format_parameter tests/test_param_security.py:116-118, ::TestTemporalSubclassRendering
datetime.time (하위 클래스 포함) TIME'HH:MM:SS' — 마이크로초와 tzinfo 버림 _cursor_common.format_parameter tests/test_param_security.py:120-122, ::TestTemporalSubclassRendering
pycubrid.types.Set / Multiset / Sequence SET{e1, e2, ...} / MULTISET{...} / SEQUENCE{...} (각 키워드는 비어 있으면 KEYWORD{}로 렌더링됨, 예: SET{}); 각 원소는 연결의 이스케이프 모드로 이 표의 행에 따라 렌더링. 중첩된 타입 지정 컬렉션은 ProgrammingError 발생 (현재 메시지: "nested collection parameters are not supported"); 원소로 쓴 일반 컨테이너는 아래와 같이 거부. 타입 지정 컬렉션 파라미터 참고 _cursor_common.py format_parameter의 타입 지정 컬렉션 분기 tests/test_typed_collections.py; tests/test_replay_parity.py::typed_collection_parameters, ::executemany_typed_collection_parameters, ::typed_collection_backslash_escape_processing; tests/test_integration_collections.py::TestTypedCollectionParameters
그 외 전부 (__class__로만 지원 타입인 척하는 객체 포함) ProgrammingError (현재 메시지: "unsupported parameter type") _cursor_common.format_parameter tests/test_param_security.py:128-130, ::TestClassSpoofing; tests/test_cursor.py:233-235

정수는 float로 변환하지 않고 바로 10진수 문자열로 변환합니다. 10**1000과 -(10**1000)처럼 float 범위를 초과하는 값도 포함됩니다. 이 포맷팅 동작이 CUBRID에서 해당 값을 저장할 수 있음을 보장하지는 않습니다. 서버의 숫자 범위 제한과 Python의 정수-문자열 변환 제한은 여전히 적용됩니다. tests/test_param_security.py::TestFormatParameterTypes::test_large_int와 ::test_bind_large_int가 이 동작을 고정합니다.

숫자 하위 클래스

int, float, decimal.Decimal의 하위 클래스는 객체 자신의 str(), repr(), format()이 아니라 기반 클래스 메서드(int.__repr__, float.__repr__, format(Decimal(value), "f"))로 숫자 값에서 렌더링됩니다. #518 이전에는 str(value)로 렌더링했기 때문에 __str__을 재정의한 하위 클래스가 SQL 텍스트를 바꿀 수 있었습니다. Python 3.10에서 enum.IntEnum 멤버는 Color.RED로, enum.IntFlag 조합은 Perm.R|W로 전송되었고, __str__이 1; DROP TABLE t를 반환하는 사용자 하위 클래스는 그 텍스트를 그대로 주입했습니다. 이제 Color.RED는 1, Perm.R | Perm.W는 6으로 전송됩니다. Decimal 하위 클래스는 먼저 일반 Decimal로 변환되므로 재정의된 __format__, is_nan(), as_tuple()이 리터럴을 바꾸거나 NaN/Infinity 및 38자리 검사를 우회할 수 없습니다. bool은 int보다 먼저 검사되어 여전히 1/0으로 렌더링되며, 하위 클래스를 만들 수 없습니다. Decimal 복사가 조작 불가능한 것은 CPython의 C decimal 모듈(_decimal)을 쓸 때뿐입니다. 순수 Python 대체 구현(_pydecimal)은 _sign, _int, _exp를 일반 속성 읽기로 복사하므로 하위 클래스가 이를 위조할 수 있습니다(#528). C 모듈이 없으면 Decimal 하위 클래스는 ProgrammingError를 발생시키며, 일반 Decimal은 그대로 허용됩니다. tests/test_param_security.py::TestFormatParameterTypes::test_numeric_subclass_renders_by_value, ::TestPureDecimalFallback, tests/test_parity_integration.py::TestParityNumericSubclassLiterals가 이 동작을 고정합니다.

텍스트, 바이너리, 날짜/시간 하위 클래스

str, bytes, bytearray, datetime.datetime, datetime.date, datetime.time의 하위 클래스는 하위 클래스가 재정의할 수 있는 메서드를 거치지 않고 저장된 값에서 렌더링됩니다(#528).

  • str: 값을 먼저 일반 str로 복사하고(기반 클래스의 str.__str__(value) 호출), NUL/Ctrl-Z 검사와 이스케이프는 그 복사본에 수행합니다. 재정의된 replace(), __contains__(), __str__(), __format__()은 호출되지 않습니다. #528 이전에는 replace()가 x'; DROP TABLE users; --를 반환하는 하위 클래스의 텍스트가 이스케이프 없이 전송되었습니다.
  • bytes/bytearray: bytes.hex(value) / bytearray.hex(value)가 버퍼를 직접 읽으므로 재정의된 hex()나 __bytes__()는 호출되지 않습니다.
  • 날짜와 시간: strftime() 대신 기반 클래스 디스크립터(datetime.date.year, datetime.datetime.hour, ...)로 읽은 정수 필드를 명시적으로 0 채움하여 리터럴을 만듭니다. strftime(), isoformat(), year/hour/... 프로퍼티를 재정의해도 리터럴은 바뀌지 않습니다. UTC 오프셋은 기반 클래스의 datetime.datetime.utcoffset()으로 얻고, 그 days/seconds/microseconds 필드도 같은 방식으로 읽습니다.
  • 활성 C _datetime 구현이 없으면 순수 Python 디스크립터가 _year, _seconds 등의 속성을 읽으므로 하위 클래스가 값을 위조할 수 있습니다. 따라서 date, datetime, time 하위 클래스는 필드 읽기나 시간대 콜백 전에 ProgrammingError로 거부하며, 정확한 기반 클래스 값을 전달해야 합니다. tzinfo.utcoffset()이 반환한 timedelta 하위 클래스도 key 조회나 드라이버의 오프셋 필드 읽기 전에 거부합니다. 일반 대체 구현 날짜/시간 값과 일반 timedelta 오프셋은 허용하며, C 구현을 쓰는 하위 클래스의 동작은 그대로 유지됩니다.
  • 1000 미만의 연도는 4자리로 0 채움됩니다(date(99, 1, 2)는 DATE'0099-01-02'로 전송). Linux에서 strftime("%Y")는 이를 채우지 않고, CUBRID는 DATE'99-01-02'를 1999-01-02로 읽기 때문에 두 자리 연도가 잘못된 연도로 조용히 저장되었습니다(#519). 한 자리와 세 자리 연도는 우연히 왕복되었으며 값은 달라지지 않습니다.
  • tzinfo.key: 비어 있지 않은 key는 [A-Za-z0-9_+/-]+에 맞는 일반 str일 때만 DATETIMETZ 리터럴에 들어갑니다(Asia/Seoul, Etc/GMT+5, America/Port-au-Prince 등 모든 IANA 이름이 해당). 그 외에는 ProgrammingError가 발생합니다. key가 없거나 None 또는 빈 문자열이면 이전처럼 ±HH:MM 숫자 오프셋을 사용합니다.
  • 숫자 오프셋은 분 단위로 나누어떨어져야 합니다(#898). +05:30:45처럼 초나 마이크로초가 있는 오프셋은 리터럴 경로와 prepared 경로 모두에서 +05:30으로 잘라 45초 다른 시각을 저장하는 대신 DataError를 발생시킵니다. tzinfo.key 지역 이름에는 영향이 없습니다.
  • 반복되는 벽시계 시각(#819): CUBRID는 반복되는 DST 시간대의 지역 이름만 있는 리터럴을 첫 번째 발생으로 해석하므로, fold=0 값은 지역 이름만 있는 리터럴(America/New_York)을 그대로 유지하며 이전 릴리스와 바이트 단위로 같습니다. 값의 벽시계 시각에서 fold=0 오프셋이 fold=1 오프셋보다 크고 value.fold가 1이면, 리터럴은 두 번째 발생을 명시합니다. 두 tzname() 결과가 서로 다른 일반 str이고 두 번째 쪽이 [A-Za-z]{3,6}에 맞으면 <key> <약어> (America/New_York EST), 아니면 두 번째 발생의 ±HH:MM 오프셋만(-05:00) 보냅니다. 이 경우 지역은 사라지지만 시점은 유지됩니다. tzname() 결과는 이 패턴에 맞을 때만 SQL에 들어갑니다. 그 외 시각은 지역 이름만 유지합니다. 건너뛴 시각(봄철 시간 변경, fold=1 오프셋이 더 큼)도 지역 이름만 보내며, CUBRID 10.2와 11.4는 어떤 리터럴 형식이든 이를 거부합니다(Invalid utime).
  • 시간대 콜백, key 조회, 오프셋 필드 읽기에서 발생한 일반 예외는 원래 예외를 원인으로 보존하는 ProgrammingError로 보고합니다(현재 메시지: "invalid tzinfo on datetime parameter"). 호출자가 제어하는 예외 텍스트를 포맷하지 않습니다. 잘못된 key 값은 위의 구체적인 오류를 유지하며, 오프셋이 None이면 key를 읽지 않고 계속 naive DATETIME으로 렌더링합니다. BaseException 중단은 그대로 전달됩니다.

타입 판별은 재정의된 __class__도 믿는 isinstance()가 아니라 type(value)를 사용합니다. __class__로만 지원 타입인 척하는 객체(예: 투명 프록시)는 ProgrammingError("unsupported parameter type")를 발생시키므로 바인딩 전에 풀어서 전달하세요. escape_string()도 str이 아닌 인자에 ProgrammingError를 발생시킵니다.

일반 값의 렌더링은 연도 0 채움을 제외하면 이전과 같습니다. tests/test_param_security.py::TestPlainLiteralsUnchanged, ::TestStrSubclassEscaping, ::TestBinarySubclassRendering, ::TestTemporalSubclassRendering, ::TestTzinfoKey, ::TestHostileTzinfo, ::test_temporal_subclasses_in_real_pure_python_fallback, ::TestClassSpoofing, 그리고 CUBRID 10.2와 11.4(sync, async)에서 tests/test_parity_integration.py::TestParityLiteralHardening이 이 동작을 고정합니다. 반복되는 DST 시각 동작은 tests/test_param_security.py::TestRepeatedWallTime과, CUBRID 10.2와 11.4(sync, async, DATETIMETZ와 TIMESTAMPTZ)에서 tests/test_integration_timezone.py::test_repeated_hour_binds_both_occurrences가 고정합니다.

Decimal 파라미터

유한한 decimal.Decimal은 지수 없이 고정소수점 표기로 렌더링됩니다. Decimal("1E-7")은 0.0000001, Decimal("1E+5")는 100000이 됩니다. CUBRID는 E가 들어간 숫자 리터럴을 DOUBLE로 해석하므로, 이전의 str(value) 렌더링(1E-7)은 이런 값을 조용히 부동소수점으로 바꾸고 삽입 시 자릿수를 잃었습니다(#517). 부호, 후행 0, scale은 작성된 그대로 유지됩니다. Decimal("1.10")은 1.10(CUBRID 타입 NUMERIC(3,2)), Decimal("-0.00")은 -0.00으로 전송됩니다.

CUBRID는 최대 38자리(NUMERIC 최대 정밀도)의 고정소수점 숫자 리터럴만 받고, 더 긴 리터럴은 오류 -494 "Invalid numeric"으로 거부합니다. 자릿수는 렌더링된 리터럴 기준입니다. 0이 아닌 정수부의 모든 자릿수와 소수부의 모든 자릿수를 세며, 소수부 앞쪽의 0(0.0000001은 7자리)과 후행 0도 포함합니다. 정수부가 0 하나뿐이면 세지 않습니다. 고정소수점 리터럴이 38자리를 넘는 Decimal은 DOUBLE로 대체되지 않고, 아무것도 전송하기 전에 DataError를 발생시킵니다. Decimal("1E-39"), 유효숫자 39자리, Decimal("1E+999999999") 같은 매우 큰 지수가 여기에 해당하며, 큰 지수는 펼치지 않고 거부합니다. 이런 값은 바인딩 전에 반올림하거나 quantize하고, DOUBLE 의미가 목적이라면 float를 바인딩하세요.

소수부가 있는 리터럴은 서버에서 NUMERIC(p,s)입니다. 소수부 없는 정수 값 (Decimal("42"), Decimal("1E+5"))은 정수 리터럴로 렌더링되며, CUBRID가 크기에 따라 INTEGER, BIGINT, NUMERIC(p,0)으로 타입을 정하고 값은 정확히 유지됩니다. 대상 컬럼의 scale이 리터럴보다 작으면 CUBRID가 다른 리터럴과 마찬가지로 대입 시 반올림합니다(비보장과 명시적 한계 참고). NaN과 Infinity는 계속 ProgrammingError를 발생시킵니다.

tests/test_param_security.py::TestFormatParameterTypes::test_decimal_plain_notation, ::test_decimal_precision_38_accepted, ::test_decimal_precision_over_38_raises, ::test_bind_decimal_plain_notation이 이 동작을 고정하며, CUBRID 10.2와 11.4에서 (동기·비동기) tests/test_parity_integration.py::TestParityDecimalLiterals가 실제 서버로 검증합니다.

타입 지정 컬렉션 파라미터

원소를 pycubrid.types.Set, Multiset, Sequence(pycubrid.Set, pycubrid.Multiset, pycubrid.Sequence로도 export)로 감싸면 일반 동기/비동기 커서로 CUBRID 컬렉션을 바인딩할 수 있습니다(#567):

from pycubrid.types import Multiset, Sequence, Set

cur.execute(
    "INSERT INTO t (tags, words, steps) VALUES (?, ?, ?)",
    (Set([1, 2, 3]), Multiset(["a", "a"]), Sequence([3, 1, 2])),
)
cur.execute("SELECT id FROM t WHERE tags SUBSETEQ ?", (Set([1, 2, 3, 4]),))
  • 각 타입은 iterable 하나를 받아 원소를 tuple(.elements)로 저장합니다. 객체는 불변이고, 같은 타입이면서 원소가 같은 경우에만 같다고 비교되며, 하위 클래스를 만들 수 없습니다. 단일 str, bytes, bytearray 인자는 문자 단위로 쪼개지 않고 TypeError를 발생시킵니다.
  • 리터럴 키워드는 타입을 따릅니다: SET{...}, MULTISET{...}, SEQUENCE{...}(CUBRID의 LIST{...}와 같은 타입). 컬렉션 의미는 서버가 적용합니다: SET은 중복을 제거하고, MULTISET은 중복은 유지하지만 순서는 유지하지 않으며, SEQUENCE는 둘 다 유지합니다.
  • 모든 원소는 스칼라 파라미터와 같은 재정의 방지 렌더러를 거치므로, 원소 타입은 위 표의 스칼라 행(None, bool, int, float, Decimal, str, bytes, bytearray, date, time, datetime)과 정확히 같고 하위 클래스 보호도 동일합니다. 원소는 리터럴과 마찬가지로 서버가 컬럼의 원소 타입으로 변환합니다.
  • 중첩 컬렉션은 거부됩니다(ProgrammingError): 다른 타입 지정 컬렉션 안의 타입 지정 컬렉션, 또는 일반 list/tuple/set/frozenset/dict 원소.
  • 세 클래스 모두 생성 시 dict를 거부합니다(TypeError): dict를 순회하면 키만 쓰이고 값은 조용히 버려지기 때문입니다. Sequence는 추가로 set/frozenset을 거부합니다(TypeError): 순회 순서가 보장되지 않아 실행마다 Sequence의 원소 순서가 달라질 수 있기 때문입니다. Set과 Multiset은 서버 측 의미 자체가 입력 순서에 의존하지 않으므로 set/frozenset을 그대로 받습니다.
  • executemany()도 각 파라미터 집합에서 타입 지정 컬렉션을 받으며, DML 배치 경로(EXECUTE_BATCH)도 포함합니다.
  • 이 인스턴스들은 불변이며 copy.copy()(항상 같은 객체를 반환), copy.deepcopy()(모든 원소가 그 자체로 불변이면 같은 객체를 반환하고, bytearray처럼 가변인 원소가 있으면 원소까지 독립적으로 복사한 별개의 객체를 반환), pickle에 안전합니다. 기존 인스턴스에서 __init__을 다시 호출해도 변경할 수 없습니다.
  • 조회 동작은 바뀌지 않습니다: decode_collections=True이면 SET 컬럼은 여전히 frozenset으로, MULTISET/SEQUENCE는 list로 디코딩됩니다(아니면 raw bytes). 디코딩된 값은 이 타입으로 다시 감싸지지 않으므로, 바인딩하려면 다시 감싸세요(예: Set(row[0])).

바인딩 값으로 명시적으로 미지원

  • datetime.timedelta — 분기가 없음; ProgrammingError("unsupported parameter type") 발생.
  • pycubrid.Lob — Lob 인스턴스는 SQL 리터럴로 변환되지 않습니다. BLOB/BIT 타입 컬럼에는 raw bytes를 삽입하고, 대형 객체 워크플로우에는 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, or wrap the elements in pycubrid.types.Set, Multiset or Sequence to bind a CUBRID collection). 자동 IN (?, ?, ?) 확장은 없습니다. SQL에 플레이스홀더를 명시적으로 펼치세요. CUBRID 컬렉션을 바인딩하려면 원소를 타입 지정 컬렉션 파라미터로 감싸세요. 메시지도 이를 안내합니다.
  • 임의의 Python 객체 — ProgrammingError("unsupported parameter type") 발생.

Prepared 호스트 변수 매핑

이 절은 위의 리터럴 계약을 바꾸지 않습니다. 명시적 pycubrid.compat.native 커서의 bind_param()이 FC3로 보내는 타입 지정 값을 설명합니다(pycubrid/_prepared_binding.py _encode_prepared_scalar, pycubrid.protocol에서 다시 내보냄). #840부터 이 커서는 타입 지정 컬렉션(bind_set()으로 바인딩)과 빈 bytes를 제외하고 리터럴 경로와 같은 Python 타입을 받습니다. 각 값은 타입 바이트 하나와 값 바이트로 전송되며, SQL로 렌더링되지 않습니다. 검사는 리터럴 경로와 같습니다. nan/inf는 ProgrammingError, 38자리가 넘게 필요한 Decimal은 DataError이며, 위조된 하위 클래스나 대체 모듈의 하위 클래스도 같은 방식으로 거부합니다. 값은 기반 클래스 메서드로 읽으므로 하위 클래스가 바이트를 바꿀 수 없습니다. 오류 메시지에는 값이 들어가지 않습니다.

Python 값 리터럴 경로 Prepared 타입과 값 바이트
None NULL NULL (0), 값 바이트 없음
bool 1 / 0 INT (8), int32 1 / 0: 리터럴 경로의 INTEGER 값
부호 있는 32비트 범위의 int 10진 숫자 INT (8), int32 (#439 이후 변경 없음)
32비트 밖이고 부호 있는 64비트 범위의 int 10진 숫자 BIGINT (21), int64
64비트 밖이고 38자리 이하인 int 10진 숫자(NUMERIC 리터럴) NUMERIC (7), 10진 텍스트와 NUL; 자릿수가 더 많으면 DataError
float float.__repr__ DOUBLE (12), IEEE 754 binary64
decimal.Decimal 고정소수점 숫자 NUMERIC (7), 같은 고정소수점 텍스트에서 단독 선행 0을 뺀 값(.5, -.05)과 NUL
str 따옴표 리터럴 CHAR (1), 연결 charset 바이트와 NUL (변경 없음)
bytes, bytearray X'<hex>' VARBIT (6), 바이트 그대로
naive datetime.datetime DATETIME'…' DATETIME (22), 밀리초를 포함한 int16 필드 7개
tz 포함 datetime.datetime DATETIMETZ'… <tz>' DATETIMETZ (31), 필드 7개와 같은 <tz> 텍스트, NUL 없음
datetime.date DATE'…' DATE (13), 연, 월, 일과 0 필드 4개
datetime.time TIME'…' TIME (14), 0 필드 3개, 시, 분, 초와 0 필드 하나; 리터럴 경로와 같이 tzinfo는 무시

브로커가 TIME을 오프셋 6에서 읽고 길이를 검사하지 않으므로 모든 날짜/시간 값은 14바이트 DATETIME 배치를 씁니다. 마이크로초는 리터럴 경로와 같이 DATETIME에서는 밀리초로 절사하고 TIME에서는 버립니다. 브로커는 부호와 소수점을 뺀 모든 문자를 정밀도로 세므로 NUMERIC 텍스트에서 선행 0을 뺍니다. 그러지 않으면 0. 뒤에 38자리가 오는 값이 리터럴에서는 성공하고 여기서는 실패합니다.

호스트 변수는 SQL 텍스트로 파싱되지 않고 서버가 호스트 변수로 변환하므로 일부 결과는 리터럴 경로와 다릅니다. CUBRID 10.2, 11.0, 11.2, 11.4에서 확인했습니다 (tests/test_integration_prepared_wide_types.py).

  • 빈 bytes. b""와 bytearray()는 NotSupportedError가 발생합니다. 브로커는 길이 0인 값을 NULL로 읽지만, 리터럴 X''는 빈 비트 문자열입니다.
  • float. CUBRID에서 리터럴 1.5는 NUMERIC 리터럴입니다(1e-07 같은 지수 형식만 DOUBLE). prepared 값은 항상 DOUBLE입니다. 저장되는 컬럼 값은 같지만 SELECT ?나 ? / 3 같은 식의 결과 타입은 다를 수 있습니다. CUBRID는 비정규 리터럴을 거부하고 리터럴 -0.0을 NUMERIC 0으로 읽으므로, 리터럴 경로는 이 두 경우를 DOUBLE 식((m * 2.409919865102884e-181), -0.0e0)으로 보내 prepared 경로와 같은 값을 저장합니다(#988).
  • 서버 측 변환 오류. 대상 타입에 담을 수 없는 값은 리터럴 경로의 메시지 대신 오류 -494(ProgrammingError, "Cannot coerce host var to type …")로 실패합니다. 예: 범위를 벗어난 BIGINT를 INTEGER 컬럼에 넣는 경우, TIME을 DATETIME 대상에 넣는 경우.
  • 타입 지정 컬렉션. Set, Multiset, Sequence는 리터럴 경로에서는 렌더링되지만 bind_param()에서는 ProgrammingError가 발생합니다. connection.set()과 bind_set()을 사용하세요. executemany(..., prepared=True)는 이를 문자열 원소의 컬렉션으로 보냅니다(아래 참고).
  • 38자리를 넘는 정수는 I/O 전에 DataError가 발생합니다. 리터럴 경로는 숫자를 그대로 보내고 서버가 거부합니다(-494, "Invalid numeric").

pycubrid.compat.cubriddb 래퍼 커서는 여전히 int, str, None 스칼라만 받으므로 bool, float, bytes, 날짜/시간 인자는 계속 ProgrammingError가 발생합니다. int 인자는 bind_param()에 전달되므로 부호 있는 32비트를 넘는 값은 이제 DataError 대신 BIGINT 또는 NUMERIC으로 바인딩됩니다.


문자열 이스케이프

문자열 이스케이프는 escape_string(pycubrid/_cursor_common.py)이 수행합니다. 동작은 no_backslash_escapes 연결 플래그(pycubrid/_connection_common.py)에 따라 달라집니다. 기본적으로 이 플래그는 연결 시점에 라이브 서버에서 자동 협상됩니다(이스케이프 모드 협상 참고). 감지를 덮어쓰려면 pycubrid.connect(..., no_backslash_escapes=True|False)로 명시적으로 전달하세요. 자동 감지는 명시적 ping(reconnect=True) 복구를 포함해 새로 열린 물리 세션마다, 파라미터화된 SQL이 그 세션을 사용하기 전에 실행됩니다.

모든 모드에서:

  • 리터럴은 작은따옴표로 감싸집니다.
  • 입력의 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를 명시적으로 전달하면 프로브를 건너뛰고 재연결 후에도 그 선택을 유지합니다. 명시적 값이 없으면 새로 열린 물리 세션은 사용 전에 프로브하며, 정상인 같은 세션에 대한 ping()은 다시 프로브하지 않습니다. 프로브가 실패하면 직접 연결 설정은 이루어지지 않으며, ping(reconnect=True) 중에는 대체 세션을 폐기하고 False를 반환합니다. 어느 경로도 모드를 추측하거나 중단된 SQL을 재실행하지 않습니다. 동기·비동기 경로에서 세션 교체 전에 바인딩된 파라미터화된 SQL은 세션 세대가 바뀌었으면 전송 전에 거부되며, 호출자가 의도적으로 작업을 다시 시도해야 합니다. 정상적인 CAS_INFO=OUT_TRAN 응답에서는 세션을 유지하며, 요청 전 CHECK_CAS가 실패한 경우에만 재연결합니다(#485). 커서는 파라미터를 렌더링하기 전에 그 검사를 실행하며, 렌더링한 뒤에 교체된 세션을 대상으로 렌더링된 SQL은 동기·비동기 모두에서 전송 전에 거부됩니다. executemany_batch()에 직접 전달한 SQL 문자열은 호출자가 렌더링한 것이므로 세대 펜스가 적용되지 않습니다.

CUBRID 10.2 및 11.4 매뉴얼은 no_backslash_escapes를 동적으로 변경 가능한 파라미터로 분류하지 않습니다. 이 드라이버 동작은 세션별 SET 전환 지원을 의미하지 않으며, 서로 다르게 설정된 서버 간의 이기종 장애 조치를 입증하지도 않습니다. 명시적 모드는 가능한 모든 대상이 그 모드와 일치한다고 알려진 경우에만 사용해야 합니다.

리터럴 모드 (no_backslash_escapes=True)

자동 협상이 기본 CUBRID 서버(no_backslash_escapes=yes)에서 선택하는 모드입니다:

  1. 작은따옴표 doubling(' → '').
  2. 백슬래시와 제어 문자는 그대로 둡니다(서버가 일반 문자로 취급하므로 바이트 단위로 왕복합니다).
  3. NUL 거부는 여전히 적용됩니다.
  4. 결과를 작은따옴표로 감쌉니다.

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해서 서버가 다시 언이스케이프하게 합니다:

  1. 백슬래시 doubling(\ → \\).
  2. 작은따옴표 doubling(' → '').
  3. \r과 \n은 각각 백슬래시 접두(\n → \\n 등).
  4. \x1a(Ctrl-Z)는 안전한 CUBRID 리터럴 이스케이프가 없어 양쪽 이스케이프 모드에서 ProgrammingError 발생(문자열 이스케이프 참고).
  5. 결과를 작은따옴표로 감쌉니다.

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 전체로 고정됩니다.

CUBRID 브로커는 execute()의 문장을 실행하기 전에 마커를 직접 세며, 주석과 작은따옴표 문자열만 건너뜁니다. 따옴표 식별자("a?b", `a?b`, [a?b]) 안의 ?도 마커로 셉니다 (src/broker/cas_execute.c의 get_num_markers). 이런 문장은 드라이버가 올바르게 렌더링해도 원인을 설명하는 메시지와 함께 ProgrammingError(errno=-1007, error_source="cas")로 실패합니다(#990). 식별자나 별칭의 이름을 바꾸세요. tests/test_error_source.py와 라이브 서버의 tests/test_integration_quoted_identifier_marker.py로 고정됩니다. PREPARE가 브로커의 마커 개수를 보고하므로 prepared 경로도 같은 부풀려진 개수를 봅니다: executemany('INSERT INTO t (v) SELECT ? AS "q?" FROM db_root', [(5,)], prepared=True)는 드라이버의 ProgrammingError "wrong number of parameters"를 일으키고, [(5, 6)]은 실행되지만 브로커가 남는 값을 조용히 무시합니다(CUBRID 11.4에서 확인, #992).

치환 단계는 세그먼트와 렌더링된 리터럴을 연결합니다(pycubrid/_cursor_common.py:204-208); 순진한 str.replace("?", ...)를 수행하지 않으므로, 한 파라미터의 렌더링된 값에 리터럴 ?가 있어도 다음 플레이스홀더를 실수로 소비하지 않습니다.

렌더링된 값은 앞의 SQL 텍스트와 이어져 주석이 되지 않습니다(#893). -로 시작하는 리터럴(음수 int, float, Decimal; -0.0과 Decimal("-0") 포함)이 SQL 텍스트의 - 바로 뒤에 오면 둘 사이에 공백 하나를 넣습니다. 따라서 x-?에 -5를 바인딩하면 x--5가 아니라 x- -5(x - (-5))로 전송됩니다. x--5는 CUBRID가 문장의 나머지를 버리는 한 줄 주석으로 읽습니다. 그 밖에는 바뀌지 않습니다. 다른 문자 뒤의 음수 (= -5, IN (-1, -2), LIMIT -1, 1+-2), - 뒤의 음수가 아닌 값(1-2), 컬렉션 원소(SET{-1, -2}, 원소는 { 또는 , 뒤에 옴)는 이전과 똑같이 렌더링됩니다. 그 밖의 렌더링된 리터럴은 -나 /로 시작하거나 -, /, *로 끝나지 않으므로, 어떤 값도 주변 SQL과 함께 --, //, /*를 만들 수 없습니다. 플레이스홀더를 다른 SQL 텍스트에 바로 붙여 쓰면(1?, ?1, x?) 값도 그대로 붙어서 들어갑니다. 그 자리에는 공백이나 연산자를 쓰십시오. tests/test_negative_literal_comment.py와 실서버의 tests/test_integration_negative_literal.py가 이 동작을 고정합니다.


executemany

DML 동사(INSERT, UPDATE, DELETE, MERGE)에 대해 executemany는:

  1. CursorParamsMixin._bind_parameter_sets()(pycubrid/_cursor_common.py)로 모든 파라미터 행을 렌더링합니다. 첫 행에서 이스케이프 모드를 확인하고 행을 검사한 뒤 SQL을 플레이스홀더로 나누는 작업을 호출마다 한 번만 합니다(#837). 그 뒤 모든 행을 그 분할로 검사·렌더링합니다. 렌더링된 각 문자열은 그 행에 대해 _bind_parameters(sql, params)가 반환하는 것과 바이트 단위로 같고, 처음 잘못된 행은 같은 오류를 냅니다.
  2. 렌더링된 SQL 문자열 리스트를 Cursor(pycubrid/cursor.py)와 AsyncCursor(pycubrid/aio/cursor.py)의 비공개 _executemany_batch()를 통해 하나의 BatchExecutePacket으로 보냅니다.

비-DML 문장에서는 executemany가 행별 execute 루프로 폴백합니다(Cursor._executemany_loop(), 그리고 AsyncCursor.executemany() 안의 루프).

각 행은 위의 동일한 타입 매핑 규칙으로 독립적으로 바인딩됩니다.

executemany(..., prepared=True)

prepared=True(#839)에서는 SQL을 렌더링하지 않습니다. INSERT, UPDATE, DELETE, MERGE에 대해 커서는 PREPARE 한 번과, 행 순서대로 행마다 표시마다 타입 바이트와 값 하나씩을 인자로 담은 EXECUTE_ARRAY 요청(함수 코드 21) 한 번을 보냅니다. 값은 Prepared 호스트 변수 매핑을 따르며, 같은 검사와 리터럴과의 같은 차이(호스트 변수 변환, float은 DOUBLE, 빈 bytes 거부)를 가집니다. 그 밖에:

  • Set, Multiset, Sequence는 STRING 원소의 컬렉션으로 보냅니다. str 원소는 그대로, int 원소(bool 제외)는 십진 텍스트로, None은 NULL 원소로 보내며 서버가 컬럼의 원소 타입으로 변환합니다. 다른 원소 타입은 ProgrammingError를 냅니다. 10.2-11.4 브로커가 MULTISET 바인딩 종류를 거부(-454)하고 SET 종류는 중복을 버리므로 Multiset은 SEQUENCE 종류로 보냅니다.
  • 같은 연결의 pycubrid.Lob은 autocommit이 꺼져 있을 때만 핸들로 바인딩합니다.
  • 아무것도 보내기 전에 모든 행을 인코딩합니다. 거부된 값은 위치(rows[i][j])만 밝히고 내용은 밝히지 않습니다. 값마다 자기 타입을 가지므로 한 컬럼이 어떤 행에서는 int, 다음 행에서는 Decimal이나 None을 가질 수 있습니다.
  • seq_of_parameters는 어떤 iterable도 사용할 수 있습니다. 생성기는 한 번만 소비되며, 네트워크 I/O 전에 인코딩된 모든 행을 보관하므로 메모리 사용량은 스트리밍 청크가 아닌 전체 요청 크기에 따라 늘어납니다. 유한하고 크기가 제한된 배치를 사용하세요.
  • 다른 문은 NotSupportedError를 내며 리터럴 배치로 대체하지 않습니다.

비보장과 명시적 한계

다음 동작들은 명시적으로 계약 밖이며 메이저 버전 없이 변경될 수 있습니다. 호출자가 암시적으로 의존하지 않도록 나열합니다.

  • 기존 동기·비동기 커서에는 서버 측 prepared 바인딩이 없음. pycubrid.Cursor와 pycubrid.aio.AsyncCursor는 값을 클라이언트에서 SQL 텍스트로 렌더링하므로 완전한 SQL을 전송하며 타입 값 페이로드와 문장 핸들 캐시가 없습니다. 옵트인 executemany(..., prepared=True)(#839)만 타입이 있는 호스트 변수를 보내며, 호출마다 문장을 한 번 준비하고 호출 사이에 핸들을 캐시하지 않습니다. 별도의 옵트인 pycubrid.compat.native 동기 커서는 제한된 스칼라 타입 바인딩을 지원하지만, 그 성능·플랜 캐시 효과는 여기서 보장하지 않습니다.
  • 식별자는 이스케이프되지 않음. 식별자를 보간하는 코드 경로(특히 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 모드의 동기/비동기 동등성.
  • tests/test_typed_collections.py — 타입 지정 Set/Multiset/Sequence 렌더링, 원소 보호, 중첩 컬렉션 거부; 동기/비동기 동등성은 tests/test_replay_parity.py::typed_collection_parameters.
  • tests/test_execute_array.py — executemany(..., prepared=True): EXECUTE_ARRAY 요청·응답 바이트, 행별 값 인코딩과 위치 메시지, 문과 LOB 규칙, 동기/비동기 핸들 소유.
  • tests/test_integration_execute_array.py — CUBRID 10.2-11.4에서의 실제 준비된 배치: 리터럴 배치와 비교한 부분 실패, autocommit, rowcount, 타입 혼합, NULL, 컬렉션, LOB 값.

이 테스트들이 이 계약의 실행 가능한 명세를 제공합니다.


참고 자료