Skip to content

기능 지원 비교 (한국어)

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

sqlalchemy-cubrid의 역량을 MySQL, PostgreSQL, SQLite의 성숙한 SQLAlchemy 방언과 종합 비교합니다.

이 문서를 사용해 CUBRID 방언이 무엇을 지원하고 지원하지 않는지, 프로젝트의 방언 선택 시 다른 데이터베이스 백엔드와 어떻게 비교되는지 이해하세요.


목차


범례

기호 의미
✅ 완전 지원
⚠️ 부분 또는 에뮬레이트 지원
❌ 미지원

요약

기능 범주별 상위 개요.

범주 CUBRID MySQL PostgreSQL SQLite
DML ✅ ✅ ✅ ⚠️
DDL ✅ ✅ ✅ ⚠️
쿼리 기능 ✅ ✅ ✅ ✅
타입 시스템 ⚠️ ✅ ✅ ⚠️
스키마 리플렉션 ✅ ✅ ✅ ⚠️
트랜잭션 ✅ ✅ ✅ ⚠️
엔진 기능 ⚠️ ✅ ✅ ⚠️

DML — 데이터 조작 언어

기능 CUBRID MySQL PostgreSQL SQLite
INSERT … RETURNING ❌ ❌ ✅ ✅
UPDATE … RETURNING ❌ ❌ ✅ ✅
DELETE … RETURNING ❌ ❌ ✅ ✅
INSERT … DEFAULT VALUES ✅ ❌ ✅ ✅
빈 INSERT ✅ ⚠️ ✅ ✅
다중 행 INSERT ✅ ✅ ✅ ✅
INSERT FROM SELECT ✅ ✅ ✅ ✅
ON DUPLICATE KEY UPDATE ✅ ✅ ❌ ❌
MERGE 문 ✅ ❌ ❌ ❌
REPLACE INTO ✅ ✅ ❌ ✅
FOR UPDATE (행 잠금) ✅ ✅ ✅ ❌
LIMIT를 가진 UPDATE ✅ ✅ ❌ ❌
TRUNCATE TABLE ✅ ✅ ✅ ❌
IS DISTINCT FROM ✅ ❌ ✅ ❌
Postfetch LASTROWID ✅ ✅ ❌ ✅

참고

  • RETURNING: CUBRID에는 RETURNING 절이 없습니다. 자동 생성 키를 INSERT와 같은 왕복에서 가져올 수 없어, 방언은 대신 postfetch_lastrowid = True에 의존합니다 (C 드라이버는 get_last_insert_id() / SQL 폴백, pycubrid는 cursor.lastrowid).
  • DEFAULT VALUES: CUBRID는 INSERT INTO t DEFAULT VALUES를 지원합니다. 방언은 supports_default_values = True를 설정합니다.
  • ON DUPLICATE KEY UPDATE: CUBRID는 INSERT … ON DUPLICATE KEY UPDATE를 지원합니다. 방언은 stmt.inserted 참조를 INSERT 바인드 파라미터를 다시 내보내는 방식으로 처리합니다(CUBRID는 VALUES() 함수를 지원하지 않습니다). sqlalchemy_cubrid.insert(table).on_duplicate_key_update(col=value)를 사용하세요. 사용 예는 CUBRID 전용 DML 구성 참고.
  • MERGE: CUBRID는 완전한 SQL MERGE 문을 지원합니다. .using(), .on(), .when_matched_then_update(), .when_not_matched_then_insert()와 함께 sqlalchemy_cubrid.dml.merge(target)을 사용하세요. CUBRID 전용 DML 구성 참고.
  • FOR UPDATE: CUBRID는 SELECT … FOR UPDATE [OF col1, col2]를 지원합니다. NOWAIT와 SKIP LOCKED는 미지원.
  • LIMIT를 가진 UPDATE: CUBRID와 MySQL 모두 UPDATE … LIMIT n을 지원합니다. PostgreSQL과 SQLite는 미지원.
  • 다중 테이블 UPDATE: SQLAlchemy의 다중 테이블 UPDATE 패턴은 UPDATE t1, t2 SET ... WHERE ...로 컴파일되며, CUBRID가 받는 MySQL 스타일 구문과 일치합니다. 방언은 추가 FROM 절이 필요 없기 때문에 의도적으로 update_from_clause()를 비활성화합니다.
  • TRUNCATE: CUBRID는 TRUNCATE TABLE을 지원합니다. 다른 문과 마찬가지로 연결의 트랜잭션 안에서 실행되며 커밋될 때만 반영됩니다 (SQL 텍스트 기반 오토커밋 없음 참고).
  • IS DISTINCT FROM: CUBRID에는 SQL 표준 구문이 없지만 NULL-안전 등가 연산자 <=>를 지원합니다. 방언은 a IS DISTINCT FROM b를 (a <=> b) = 0으로, a IS NOT DISTINCT FROM b를 a <=> b로 에뮬레이트하여 술어와 SELECT 프로젝션 모두에서 NULL-안전 의미를 보존합니다 (#344, #377). CUBRID의 IS는 NULL, TRUE, FALSE만 받으므로, 값과의 IS/IS NOT(예: Boolean col.is_(True))도 같은 방식(col <=> 1, (col <=> 1) = 0)으로 렌더링합니다 (#465). 불리언 조건식을 참고하세요.

DDL — 데이터 정의 언어

기능 CUBRID MySQL PostgreSQL SQLite
ALTER TABLE ✅ ✅ ✅ ⚠️
테이블 코멘트 ✅ ✅ ✅ ❌
컬럼 코멘트 ✅ ✅ ✅ ❌
CREATE … IF NOT EXISTS ✅ ✅ ✅ ✅
DROP … IF EXISTS ✅ ✅ ✅ ✅
임시 테이블 ❌ ✅ ✅ ✅
다중 스키마 ❌ ✅ ✅ ⚠️

참고

  • ALTER TABLE: CUBRID는 컬럼과 제약조건 추가/삭제를 위한 표준 ALTER TABLE을 지원합니다. SQLite는 ALTER 지원이 제한적입니다 (컬럼 추가만, 3.35 이전에는 drop/rename 불가).
  • 코멘트: CUBRID는 테이블(예: CREATE TABLE t (...) COMMENT = 'text')과 컬럼(예: col TYPE COMMENT 'text') 모두 인라인 COMMENT 구문을 지원합니다. 방언은 SetTableComment, DropTableComment, SetColumnComment DDL 구성을 구현합니다. 코멘트 리플렉션은 get_table_comment()와 get_columns()의 컬럼 코멘트로 지원됩니다.
  • IF NOT EXISTS / IF EXISTS: CUBRID는 CREATE TABLE IF NOT EXISTS와 DROP TABLE IF EXISTS를 지원합니다. 기본 SA 컴파일러가 이를 네이티브로 처리합니다. CUBRID에는 CREATE INDEX IF NOT EXISTS와 DROP INDEX IF EXISTS가 없으므로 CreateIndex(..., if_not_exists=True)와 DropIndex(..., if_exists=True)는 CompileError를 발생시킵니다(#540, #533). 대신 checkfirst=True 또는 inspect(conn).has_index()를 사용하세요.
  • 임시 테이블: CUBRID는 CREATE TEMPORARY TABLE이나 세션 범위 테이블을 지원하지 않습니다.
  • 다중 스키마: CUBRID는 단일 스키마 모델로 동작합니다. MySQL은 데이터베이스를 스키마로 사용합니다. SQLite는 데이터베이스를 attach할 수 있지만 진정한 스키마 지원은 없습니다.

쿼리 기능

기능 CUBRID MySQL PostgreSQL SQLite
공통 테이블 표현식 (WITH) ✅ ✅ ✅ ✅
재귀 CTE (WITH RECURSIVE) ✅ ✅ ✅ ✅
DML 상의 CTE ❌ ❌ ✅ ❌
윈도우 함수 ✅ ✅ ✅ ✅
NULLS FIRST / NULLS LAST ✅ ❌ ✅ ✅
GROUP_CONCAT ✅ ✅ ❌ ✅
INTERSECT ✅ ✅ ✅ ✅
EXCEPT ✅ ✅ ✅ ✅
DISTINCT ✅ ✅ ✅ ✅
LIMIT / OFFSET ✅ ✅ ✅ ✅
Lateral 조인 ❌ ❌ ✅ ❌
전문 검색 (MATCH … AGAINST) ❌ ✅ ✅ ✅
쿼리 추적 / EXPLAIN ⚠️ ✅ ✅ ✅

참고

  • CTE: CUBRID는 읽기 쿼리를 위한 WITH 절을 지원하며, test/test_integration.py의 TestIsolatedCTE849가 CUBRID 10.2, 11.0, 11.2, 11.4에서 실제로 확인합니다 (#849): 비재귀와 재귀 SELECT, CTE 안의 바인드 파라미터, CTE를 통한 values(), UPDATE / DELETE ... WHERE col = (SELECT ... FROM cte)가 기대한 행을 반환합니다. CTE 본문 안의 데이터 변경 문장(PostgreSQL 방식의 쓰기 가능 CTE, WITH x AS (INSERT … RETURNING …))은 미지원이며, 위와 같이 UPDATE나 DELETE는 서브쿼리를 통해 CTE를 읽을 수 있습니다. CUBRID에는 다중 테이블 UPDATE / DELETE가 없으므로 UPDATE ... FROM cte와 DELETE ... FROM cte는 예외를 발생시킵니다. insert().from_select(..., select(cte))와 replace().from_select(..., select(cte))는 재귀 CTE를 포함해 WITH 절을 컬럼 목록 뒤에 렌더링합니다(INSERT INTO t (...) WITH ... SELECT). CUBRID가 허용하는 순서는 이것뿐이며, WITH ... INSERT는 CUBRID 10.2와 11.4에서 구문 오류(-493)입니다 (#897). SQLAlchemy 자체의 CTETest 노드는 이 쿼리들 때문이 아니라 픽스처 정리가 CUBRID의 행 단위 외래 키 검사에서 실패하기 때문에 test/known_failures.txt에 기준선으로 남아 있습니다.
  • 재귀 CTE: CUBRID는 재귀 쿼리를 위한 WITH RECURSIVE를 지원하며, 10.2, 11.0, 11.2, 11.4에서 실제로 확인했습니다 (#849). SQLAlchemy 기본 컴파일러가 올바른 구문을 생성합니다 — 방언별 컴파일이 필요 없습니다.
  • 윈도우 함수: CUBRID는 OVER(PARTITION BY … ORDER BY …)와 함께 ROW_NUMBER(), RANK(), DENSE_RANK() 등의 윈도우 함수를 지원합니다. SA 기본 컴파일러가 이를 네이티브로 처리합니다.
  • NULLS FIRST / NULLS LAST: CUBRID는 ORDER BY col ASC NULLS FIRST와 ORDER BY col DESC NULLS LAST를 지원합니다. SA 기본 컴파일러가 네이티브로 처리합니다.
  • GROUP_CONCAT: CUBRID는 GROUP_CONCAT([DISTINCT] expr [ORDER BY …] [SEPARATOR '…'])를 지원합니다. sa.func.group_concat(column), sa.func.group_concat(column.distinct()), 또는 ORDER BY / SEPARATOR에는 literal_column() 인자를 사용하세요. GROUP_CONCAT을 참고하세요.
  • LIMIT / OFFSET: CUBRID는 MySQL 스타일 LIMIT [offset,] count 구문을 사용합니다. 오프셋만 주어지면 방언은 "남은 모든 행" 센티넬로 LIMIT offset, 4611686018427387904(2^62)를 냅니다 — 사실상 무제한이면서, CUBRID가 내부적으로 계산하는 offset + count 덧셈이 부호 있는 BIGINT 범위를 넘어 오버플로하지 않을 만큼 작습니다. CUBRID에는 FETCH FIRST n ROWS ONLY가 없으므로 Select.fetch(n)도 LIMIT n(.offset()과 함께면 LIMIT offset, n)으로 렌더링됩니다 (#867). fetch(..., percent=True)와 fetch(..., with_ties=True)는 LIMIT에 대응하는 형식이 없으므로 CompileError를 발생시킵니다.
  • 불리언 SELECT 목록 항목: CUBRID는 괄호 없는 EXISTS (...), NOT (...), a AND b / a OR b를 SELECT 목록 항목으로 거부하지만(SELECT EXISTS (SELECT 1)은 구문 오류) 괄호 안에서는 허용합니다. 방언이 이런 컬럼을 괄호로 묶으므로 select(exists().where(...)), select(~exists()...), select(and_(...))와 ORM의 session.query(q.exists()).scalar()가 동작합니다 (#966). CUBRID는 이 식들을 괄호 없는 CAST나 함수 인자, ORDER BY 항목으로도 거부하므로, 방언이 그 위치에서도(type_coerce(), 레이블, 정렬 수식자 안 포함) 괄호로 묶습니다. select(type_coerce(exists(), Boolean)), select(cast(and_(...), Integer)), select(func.coalesce(exists(), False)), column_expression()이 컬럼을 감싸는 Boolean TypeDecorator, order_by(and_(...)), order_by(exists())가 동작합니다 (#968). CAST나 함수는 SELECT 목록뿐 아니라 어디에 있든(예: WHERE, HAVING, ON, UPDATE ... SET) 이런 인자를 괄호로 묶으므로 where(func.coalesce(and_(...), 0) == 1)와 where(cast(and_(...), Integer) == 1)도 동작합니다. 윈도나 WITHIN GROUP의 ORDER BY·PARTITION BY 항목인 EXISTS (...)와 NOT (...)도 괄호로 묶으므로 over(order_by=exists()), over(partition_by=exists()), within_group(~exists())가 동작합니다. 윈도 order_by나 partition_by에 and_()나 or_() 하나만 넘기면 SQLAlchemy 자체가 모든 방언에서 항목 여러 개로 나누므로(or_()는 의미가 바뀜) 리스트 안에 넣거나(over(order_by=[and_(...)])) .self_group()을 호출하십시오. 리스트 안과 WITHIN GROUP에서는 괄호로 묶습니다 (#970). CASE의 THEN이나 ELSE 값도 검색형과 단순형(value=) case() 모두에서 괄호로 묶으므로 case((x > 0, and_(...)), else_=literal(False))와 case((x > 0, exists()), else_=literal(False))가 동작합니다 (#982). 불리언 단순형 case()의 값 피연산자도 괄호로 묶으므로 case({True: 10}, value=and_(...), else_=20)와 case({True: 10}, value=exists(), else_=20)가 동작합니다 (#987). CUBRIDdb에서는 결과가 모두 바인드 값인 CASE가 문자열을 반환합니다. 드라이버 호환성을 참고하십시오. CUBRID는 결과가 불리언이면서 ELSE가 없거나 ELSE NULL뿐인 CASE(예: WHEN이 하나뿐인 경우)를 괄호로 묶든 묶지 않든 거부할 수 있습니다("semantic error", -495). 서버 제한이므로 NULL이 아닌 else_=(예: else_=literal(False))를 넘기십시오. CUBRID 11.4는 윈도 ORDER BY나 PARTITION BY 항목으로 쓴 CASE도 무시합니다. 오류 없이 상수로 평가되므로 그 항목이 없는 것처럼 번호가 매겨집니다. 그 위치에는 CASE를 쓰지 마십시오. 불리언 값을 쓰는 CASE를 GROUP BY 항목으로 쓰면 실패하므로("search condition is not allowed as group by spec", -493) 레이블로 그룹화하십시오. 괄호 없는 WHERE·HAVING·ON 조건, GROUP BY 항목, CASE의 WHEN 조건이나 그 밖의 피연산자인 같은 식은 이전과 같이 렌더링됩니다. 지원하지 않음: 불리언 식을 GROUP BY 항목으로 쓰는 것. CUBRID는 괄호 안이라도 검색 조건을 그 위치에서 거부하므로("search condition is not allowed as group by spec", -493) group_by(a | b)와 group_by(exists())는 실패합니다. 대신 SELECT 목록에서 식에 레이블을 붙이고 그 레이블로 그룹화하십시오. 예: select(and_(...).label("k"), func.count()).group_by("k"). GROUP BY 안의 CAST나 함수도 마찬가지여서 group_by(func.coalesce(exists(), 0))는 괄호로 묶든 묶지 않든 실패합니다. CUBRID 11.4에서 검증했으며 10.2, 11.0, 11.2에서는 검증하지 않았습니다.
  • 조인 변형: INNER JOIN과 LEFT OUTER JOIN은 정상 컴파일됩니다. FULL OUTER JOIN과 LATERAL은 CUBRID가 지원하지 않아 컴파일 중 거부됩니다.
  • Lateral 조인: CUBRID는 LATERAL 서브쿼리를 지원하지 않습니다. LATERAL 키워드는 구문 오류를 일으킵니다.
  • 전문 검색: CUBRID는 MATCH … AGAINST 구문이나 전문 인덱스를 지원하지 않습니다.
  • 쿼리 추적: CUBRID는 표준 EXPLAIN 대신 SET TRACE ON / SHOW TRACE를 사용합니다. 방언은 유틸리티 함수로 trace_query()를 제공합니다 — CUBRID 전용 DML 구성 참고.

타입 시스템

표준 SQL 타입

타입 CUBRID MySQL PostgreSQL SQLite
SMALLINT ✅ ✅ ✅ ✅
INTEGER ✅ ✅ ✅ ✅
BIGINT ✅ ✅ ✅ ✅
NUMERIC / DECIMAL ✅ ✅ ✅ ✅
FLOAT ✅ ✅ ✅ ✅
DOUBLE / REAL ✅ ✅ ✅ ✅
BOOLEAN ⚠️ ⚠️ ✅ ⚠️
DATE ✅ ✅ ✅ ✅
TIME ✅ ✅ ✅ ✅
DATETIME ✅ ✅ ✅ ✅
TIMESTAMP ✅ ✅ ✅ ✅
CHAR ✅ ✅ ✅ ✅
VARCHAR ✅ ✅ ✅ ✅
NCHAR / NVARCHAR ✅ ⚠️ ❌ ❌
TEXT ✅ ✅ ✅ ✅
BLOB ⚠️ ✅ ✅ ✅
CLOB ⚠️ ✅ ✅ ✅
BIT / BIT VARYING ✅ ✅ ✅ ❌

확장 타입

타입 CUBRID MySQL PostgreSQL SQLite
ENUM ✅ ✅ ✅ ❌
JSON ✅ ✅ ✅ ⚠️
ARRAY ❌ ❌ ✅ ❌
UUID ❌ ❌ ✅ ❌
INTERVAL ❌ ❌ ✅ ❌
HSTORE ❌ ❌ ✅ ❌

참고

  • BOOLEAN: CUBRID는 BOOLEAN을 SMALLINT로 매핑합니다. MySQL은 TINYINT(1)로 매핑합니다. SQLite는 불리언을 정수로 저장합니다. PostgreSQL만 네이티브 BOOLEAN 타입을 가집니다.
  • NCHAR / NVARCHAR: CUBRID는 일급 국가 문자 타입을 가집니다. MySQL은 컬럼 문자셋으로 국가 문자를 처리합니다. PostgreSQL과 SQLite에는 별도의 국가 문자 타입이 없습니다.
  • JSON: CUBRID 10.2+는 25개 이상의 JSON 함수를 가진 네이티브 JSON 지원(RFC 7159)이 있습니다. 방언은 JSON 타입, JSON_EXTRACT를 통한 col["key"] 경로 표현식, 타입별 접근(as_string(), as_integer(), as_float(), 그리고 NUMERIC(p,s)로 캐스팅해 Decimal을 반환하는 as_numeric(p, s))을 지원합니다. as_numeric()은 JSON 숫자, 일반 십진수 형식의 숫자 문자열("15.5"), JSON true/false(1.00/0.00)를 캐스팅합니다. 지수 표기 문자열("1e3")은 CUBRID 오류 -181, NUMERIC(p,s) 범위를 벗어난 값은 -427을 발생시키므로 이런 값에는 as_float()를 사용하세요. 숫자가 아닌 문자열("abc")은 as_numeric()과 as_float() 모두에서 -181이 발생하므로 캐스팅 전에 검증하거나 걸러내세요. MySQL(5.7+)과 PostgreSQL도 네이티브 JSON 지원이 있습니다. SQLite는 JSON 함수는 있지만 전용 컬럼 타입은 없습니다.
  • ARRAY: CUBRID는 유사 목적을 수행하지만 SQL 표준 배열은 아닌 컬렉션 타입(SET, MULTISET, SEQUENCE)을 사용합니다. PostgreSQL은 네이티브 ARRAY[] 지원이 있습니다.
  • CLOB: CUBRID와 MySQL은 명시적 CLOB 타입이 있습니다. PostgreSQL은 TEXT(무제한 길이)를 사용합니다. SQLite는 모든 텍스트를 TEXT로 저장합니다.
  • CUBRID의 BLOB / CLOB (⚠️): 타입 자체는 구현되어 있습니다. BLOB, CLOB, LargeBinary는 CUBRID BLOB / CLOB DDL로 컴파일되고 리플렉션되며, bytes / str을 바인딩하면 전체 값이 저장되고 NULL은 None으로 왕복합니다. 부분 지원인 부분은 일반 Core 또는 ORM SELECT가 반환하는 값입니다. NULL이 아닌 BLOB / CLOB 컬럼은 cubrid://, cubrid+pycubrid://, cubrid+aiopycubrid:// 모두에서 bytes / str이 아니라 드라이버의 LOB 로케이터로 반환됩니다(LargeBinary / BLOB은 이후 SQLAlchemy 결과 처리기가 TypeError를 발생시킵니다). 이는 CUBRID 저장 한계가 아니라 드라이버 조회 한계이며, test/test_lob_value_contract.py는 이 조회 사례를 strict xfail로 유지합니다. 내용을 읽으려면 서버에서 변환(CLOB_TO_CHAR(col), BLOB_TO_BIT(col))하거나 큰 텍스트는 Text(CUBRID STRING)에 저장하세요. 드라이버 호환성, 알려진 문제 6과 타입을 참고하세요.

스키마 리플렉션

기능 CUBRID MySQL PostgreSQL SQLite
테이블 이름 ✅ ✅ ✅ ✅
컬럼 정보 ✅ ✅ ✅ ✅
기본 키 ✅ ✅ ✅ ✅
외래 키 ✅ ✅ ✅ ✅
인덱스 ✅ ✅ ✅ ✅
유니크 제약 ✅ ✅ ✅ ✅
CHECK 제약 ❌ ✅ ✅ ❌
테이블 코멘트 ✅ ✅ ✅ ❌
컬럼 코멘트 ✅ ✅ ✅ ❌
뷰 이름 ✅ ✅ ✅ ✅
뷰 정의 ✅ ✅ ✅ ✅
스키마 이름 ❌ ✅ ✅ ❌
시퀀스 ❌ ❌ ✅ ❌
has_table ✅ ✅ ✅ ✅
has_index ✅ ❌ ✅ ✅
has_sequence ❌ ❌ ✅ ❌

참고

  • CHECK 제약: CUBRID는 CHECK 제약 구문을 파싱하지만 런타임에 강제하지 않습니다. 오해를 일으키는 메타데이터의 리플렉션을 피하기 위해 방언은 의도적으로 get_check_constraints()에서 빈 리스트를 반환합니다.
  • 테이블 코멘트: db_class.comment 시스템 카탈로그 컬럼을 조회하는 get_table_comment()로 리플렉트됩니다.
  • 컬럼 코멘트: db_attribute.comment 카탈로그 뷰 컬럼을 조회하는 get_columns()로 리플렉트됩니다. 각 컬럼 dict의 "comment" 키로 반환됩니다.
  • has_index: CUBRID 방언은 db_index를 조회해 has_index()를 구현합니다. MySQL SA 방언은 전용 has_index() 메서드를 제공하지 않습니다. CUBRID는 따옴표로 감싼 식별자도 소문자로 저장하므로, has_table()과 has_index()는 대소문자가 섞인 이름을 소문자로 저장된 이름과도 비교합니다(#543). 따라서 Index("IX_Mixed", Table("Users", ...))에서도 Index.drop(checkfirst=True)가 동작합니다.
  • 인덱스 형태: get_indexes()는 함수 기반 키(CREATE INDEX ... (LOWER(a)))를 column_names에 None을 두고 expressions로, DESC 키를 column_sorting으로, 필터 인덱스(... WHERE c > 3)를 dialect_options["cubrid_where"]로, 키 접두사(a(5))를 dialect_options["cubrid_length"]로 보고합니다 (#869). 정보는 SHOW INDEXES의 Func, Collation, Sub_part 컬럼과 db_index.filter_expression에서 읽습니다. CUBRID는 식과 필터의 컬럼을 테이블로 한정해 출력하므로(lower([dba.t].[a]), [dba.t].c>3) 한정자를 제거해 lower([a])와 c>3으로 리플렉트하며, 리플렉트한 테이블은 같은 인덱스를 다시 만듭니다. 같은 옵션으로 인덱스를 만들 수 있습니다: Index("ix", t.c.c, cubrid_where=t.c.c > 3)은 CREATE INDEX ix ON t (c) WHERE c > 3을, Index("ix", t.c.a, cubrid_length={"a": 5})(모든 컬럼 키에는 cubrid_length=5)는 CREATE INDEX ix ON t (a(5))를 냅니다. CUBRID는 UNIQUE 필터 인덱스를 거부하므로 unique=True와 cubrid_where를 함께 쓰면 CompileError가 발생하며, 식 키의 접두사 길이도 마찬가지입니다. Alembic autogenerate는 이 인덱스들을 정규화한 키, 정렬 순서, 필터, 접두사 길이로 비교하므로(문자열 리터럴은 쓰인 그대로 비교) 지원하는 모든 Alembic에서 바뀌지 않은 인덱스는 다시 제안되지 않습니다. 정렬 순서, 필터, 접두사 길이가 바뀐 인덱스를 교체하려면 방언의 인덱스 비교를 호출하는 첫 릴리스인 Alembic 1.12 이상이 필요합니다.
  • 유니크 제약과 유니크 인덱스: CUBRID는 UNIQUE 제약을 유니크 인덱스로 구현하며 CREATE UNIQUE INDEX와 구분할 수 없습니다. 따라서 MySQL과 마찬가지로 각 항목은 get_indexes()와 get_unique_constraints() 모두에 나타나며, 유니크 제약 항목에는 해당 인덱스 이름을 담은 duplicates_index가 있습니다. Table 리플렉션(및 Alembic autogenerate)은 이 키를 사용해 유니크 인덱스만 남깁니다. 뷰에 대해서는 get_indexes()가 빈 리스트를 반환합니다.
  • 존재하지 않는 테이블과 뷰: get_columns(), get_pk_constraint(), get_foreign_keys(), get_indexes(), get_unique_constraints(), get_table_comment(), get_view_definition()은 존재하지 않는 객체에 대해 NoSuchTableError를 발생시키므로 get_multi_*() 변형은 해당 객체를 결과에서 제외합니다. get_view_definition()은 테이블에 대해서도 이 예외를 발생시킵니다. 뷰에 대해서는 get_foreign_keys()와 get_unique_constraints()가 SHOW CREATE TABLE을 실행하지 않고 빈 리스트를 반환합니다.
  • 리플렉션 소스: 리플렉션은 여러 소스에 분산되어 있습니다: 컬럼/코멘트는 SHOW COLUMNS IN + db_attribute, 기본 키(이름과 키 순서의 모든 컬럼)는 db_index + db_index_key, 외래 키는 SHOW CREATE TABLE 파싱, 유니크 제약은 db_index + SHOW INDEXES IN(db_index에 테이블의 인덱스가 하나도 없거나, CUBRID 11.2부터 이름이 유니크 인덱스가 보이지 않는 다른 소유자의 클래스로 해석될 때만 SHOW CREATE TABLE 파싱으로 대체하며(유니크 인덱스가 보이면 SHOW INDEXES IN이 먼저 NoSuchTableError를 발생시키고, 대체 경로도 같은 오류를 발생시킴), 그 밖에 기본 키, 외래 키 또는 다른 인덱스는 있지만 유니크 인덱스가 없는 테이블은 카탈로그만으로 판단, #610), 인덱스는 SHOW INDEXES IN + db_index, 뷰 정의는 SHOW CREATE VIEW, 테이블/뷰 이름과 테이블 코멘트는 db_class.
  • 외래 키 DDL 문법: get_foreign_keys()는 지원 서버(10.2, 11.0, 11.2, 11.4)의 SHOW CREATE TABLE 출력에 의존하며, 이 출력은 test/fixtures/show_create_table/에 기록되어 있고 test/test_show_create_table_server_output.py가 실제 서버와 비교합니다 (#611): 모든 식별자는 대괄호로 출력되고 공백, 쉼표, 괄호는 그대로 유지됩니다. 각 외래 키는 두 동작을 모두 ON DELETE, ON UPDATE 순서로 출력하며 기본값은 RESTRICT로 출력됩니다. 동작은 CASCADE, SET NULL, NO ACTION, RESTRICT입니다(서버는 ON UPDATE CASCADE를 거부하며 SET DEFAULT는 없습니다). 참조 테이블은 11.2부터 소유자가 붙어([dba.parent]) 출력되고 10.2와 11.0에서는 붙지 않으며, referred_table에서는 소유자를 제거합니다. UNIQUE 제약과 CREATE UNIQUE INDEX 인덱스는 모두 UNIQUE KEY로 출력됩니다.
  • 테이블과 뷰 목록: get_table_names(), get_view_names(), has_table()은 db_class를 읽습니다. CUBRID 11.2부터 이 뷰에는 사용자가 읽을 수 있는 모든 소유자의 클래스가 나타나지만 소유자를 붙이지 않은 이름은 현재 사용자의 스키마에서만 찾으므로, 이 메서드들은 owner_name = CURRENT_USER로 제한됩니다 (#864). 따라서 같은 이름의 다른 소유자 테이블 때문에 create_all()이 현재 사용자의 테이블 생성을 건너뛰거나, MetaData.reflect()가 그 테이블에서 NoSuchTableError로 중단되거나, 두 소유자의 같은 이름 테이블이 중복으로 나열되지 않습니다. 기본 스키마가 아닌 schema= 인자는 다른 모든 리플렉션 메서드와 마찬가지로 이름을 반환하지 않습니다. CUBRID 10.2와 11.0의 클래스 이름은 전역이므로 목록 동작은 바뀌지 않습니다.
  • 리플렉트된 서버 기본값: get_columns()는 각 기본값을 CreateTable과 Alembic이 다시 낼 수 있는 SQL로 보고합니다 (#865). 숫자 컬럼의 숫자 기본값은 SHOW COLUMNS가 출력한 그대로(5, -3, 1.50) 유지합니다. 문자열 컬럼의 '5'(SHOW COLUMNS는 이것도 5로 출력)를 포함한 그 밖의 기본값은 SHOW CREATE TABLE에서 읽으며, 이때 get_columns()는 테이블당 한 번 이를 실행합니다. SHOW COLUMNS는 문자열 기본값을 따옴표 없이 출력하므로 'it''s', '', 문자열 'NULL'이 각각 it''s, 빈 문자열, NULL로, 날짜가 01/02/2020으로, 'CURRENT_USER'와 USER가 모두 CURRENT_USER로 리플렉트되었습니다. 이제는 'it''s', '', 'NULL', date '01/02/2020', 'CURRENT_USER', CURRENT_USER로 리플렉트됩니다. SYS_DATETIME이나 TO_CHAR(SYS_DATETIME, 'YYYY') 같은 함수, 문자셋 접두사가 붙은 리터럴(_utf8'q', N'n'), 비트 문자열(X'a0')은 서버 표기를 유지합니다. 기본값이 없는 컬럼은 여전히 None입니다. 뷰는 SHOW CREATE TABLE을 실행하지 않으며, 출력을 해석할 수 없으면 SHOW COLUMNS 텍스트를 유지합니다.
  • DBA가 아닌 사용자의 리플렉션: 리플렉션은 모든 사용자가 읽을 수 있는 카탈로그 뷰(db_class, db_index, db_index_key, db_attribute)만 읽고, DBA만 읽을 수 있는 _db_index, _db_index_key, _db_attribute 카탈로그 테이블은 읽지 않습니다. 따라서 DBA가 아닌 사용자도 DBA와 같은 인덱스, 기본 키, 유니크 제약, has_index() 결과와 컬럼 코멘트를 얻습니다. 테이블 이름은 SHOW COLUMNS IN <name>과 같이 입력한 그대로 또는 소문자로 변환해 비교합니다. CUBRID 11.2부터 이 뷰에는 다른 소유자의 같은 이름 클래스도 나타나므로, 리플렉트하는 클래스의 소유자 행만 사용합니다(db_class와 마찬가지로 현재 사용자의 클래스가 우선). 카탈로그 조회가 실패하면 불완전한 메타데이터로 조용히 대체하지 않고 예외를 발생시킵니다.
  • SHOW CREATE TABLE 실패: get_foreign_keys()(와 get_unique_constraints()의 SHOW CREATE TABLE 대체 경로)는 SHOW CREATE TABLE이 성공했고 DDL에 해당 제약이 없을 때만 빈 리스트를 반환합니다. Unknown class이거나 카탈로그에 있는 테이블에 대해 행이 반환되지 않으면 NoSuchTableError를 발생시키고, 그 밖의 실패(연결 끊김, 권한 오류, 드라이버 오류)는 로그를 남기고 "제약 없음"으로 보고하는 대신 그대로 전파합니다. 이전 동작 때문에 Alembic autogenerate가 이미 존재하는 외래 키에 대해 add_fk를 생성했습니다 (#589). CUBRID는 사용자가 SELECT 권한을 가진 테이블만 db_class에 보여 주고, 그런 테이블에 대한 SHOW CREATE TABLE은 권한 오류로 실패하지 않으므로(CUBRID 10.2와 11.4에서 DBA가 아닌 사용자로 확인) DBA가 아닌 사용자에게도 실패를 허용하지 않습니다. CUBRID 11.2부터 소유자를 붙이지 않은 이름은 현재 사용자의 스키마에서 찾으므로, 다른 소유자의 테이블은 현재 사용자에게 권한이 부여되어 있어도 get_columns()와 마찬가지로 NoSuchTableError를 발생시킵니다. 따라서 get_unique_constraints()는 카탈로그에 그런 테이블의 인덱스는 있지만 유니크 인덱스가 없으면 SHOW CREATE TABLE을 계속 읽고, 유니크 인덱스가 있으면 DDL을 읽기 전에 SHOW INDEXES IN이 NoSuchTableError를 발생시킵니다. 현재 사용자의 테이블(11.2 이전에는 보이는 모든 테이블)에 대해 카탈로그에 인덱스는 있지만 유니크 인덱스가 없으면 get_unique_constraints()는 SHOW CREATE TABLE을 실행하지 않고 []를 반환합니다 (#610). 카탈로그에 있는 유니크 인덱스의 키 컬럼을 SHOW INDEXES가 반환하지 않으면, get_unique_constraints()는 Alembic이 제약 누락으로 읽을 불완전한 결과를 보고하는 대신 해석되지 않은 인덱스와 해석된 인덱스를 알려 주는 InvalidRequestError를 발생시킵니다 (#848).

트랜잭션과 연결

기능 CUBRID MySQL PostgreSQL SQLite
격리 수준 관리 ✅ ✅ ✅ ✅
세이브포인트 ✅ ✅ ✅ ✅
2단계 커밋 ❌ ✅ ✅ ❌
서버 측 커서 ❌ ✅ ✅ ❌
SQL 텍스트 기반 오토커밋 감지 ❌ ❌ ❌ ❌
연결 수준 인코딩 ❌ ✅ ✅ ❌

CUBRID 격리 수준

CUBRID의 MVCC 엔진(10.0+)은 세 가지 격리 수준을 지원합니다:

수준 설명
SERIALIZABLE (6) 완전 직렬화
REPEATABLE READ (5) 트랜잭션 내 반복 가능한 읽기
READ COMMITTED (4, 기본) 읽기는 커밋된 데이터만 봄. 반복 불가능 리드 가능

SQLAlchemy의 AUTOCOMMIT 수준도 모든 드라이버에서 받으며, 드라이버를 오토커밋 모드로 전환합니다. 격리 수준을 참고하세요.

참고

  • 2단계 커밋: CUBRID는 XA를 통한 분산 트랜잭션을 지원하지 않습니다.
  • 서버 측 커서: CUBRID Python 드라이버는 서버 측 커서 기능을 노출하지 않습니다.
  • SQL 텍스트 기반 오토커밋 감지: SQLAlchemy 2.x는 어떤 방언에서도 문장 텍스트를 검사해 커밋 여부를 결정하지 않습니다. DML과 DDL은 conn.commit(), engine.begin() 블록, Session 커밋을 통해서만 커밋됩니다. SQL 텍스트 기반 오토커밋 없음을 참고하세요.
  • 세이브포인트: CUBRID는 SAVEPOINT와 ROLLBACK TO SAVEPOINT를 지원합니다. RELEASE SAVEPOINT는 미지원 — 방언은 do_release_savepoint()를 no-op로 구현합니다.

방언 엔진 기능

기능 CUBRID MySQL PostgreSQL SQLite
문장 캐싱 ✅ ✅ ✅ ✅
네이티브 enum ✅ ✅ ✅ ❌
네이티브 불리언 ❌ ❌ ✅ ❌
네이티브 decimal ✅ ✅ ✅ ❌
시퀀스 ❌ ❌ ✅ ❌
ON UPDATE CASCADE ✅ ✅ ✅ ✅
ON DELETE CASCADE ✅ ✅ ✅ ✅
자기 참조 FK ✅ ✅ ✅ ✅
독립 연결 ✅ ✅ ✅ ✅
유니코드 DDL ✅ ✅ ✅ ✅
이름 정규화 ✅ ❌ ✅ ❌

참고

  • 문장 캐싱: supports_statement_cache = True. 방언은 SQLAlchemy 2.0의 컴파일 캐시와 완전 호환됩니다.
  • 이름 정규화: CUBRID는 인용되지 않은 식별자를 소문자로 폴딩하며, 이는 대소문자를 구분하지 않는 이름에 대한 SQLAlchemy의 소문자 규칙과 이미 일치합니다. 따라서 방언은 requires_name_normalize = False로 설정하며 SQLAlchemy의 대문자 이름 정규화를 적용하지 않습니다.
  • 최대 식별자 길이: CUBRID는 254자까지 식별자를 허용합니다 — MySQL(64)이나 PostgreSQL(63)보다 상당히 깁니다.
  • 결과 메타데이터 (cursor.description): 방언은 드라이버의 cursor.description을 그대로 전달하며(CursorResult.cursor.description) 어떤 값도 정규화하지 않습니다. CUBRID 10.2 및 11.4에서 동기·비동기 pycubrid와 CUBRIDdb로 실제 검증했습니다(#482). Result.keys()는 text()의 별칭과 표현식을 포함해 description 이름과 같습니다(1 + 1의 이름은 1+1). 스칼라 타입 코드는 모든 드라이버에서 CUBRID 코드입니다(예: VARCHAR 2, NUMERIC 7, INTEGER 8, DOUBLE 12, DATE 13, TIMESTAMP 15, BIGINT 21). 동기와 비동기 pycubrid는 같은 값을 보고합니다. 드라이버 차이: 컬렉션 컬럼은 pycubrid에서 SET 16 / MULTISET 17 / SEQUENCE 18이지만 CUBRIDdb에서는 CCI 복합 코드(비트 0x60의 종류와 요소 타입의 조합, 즉 SET(INTEGER)는 40)입니다. null_ok는 pycubrid에서 bool, CUBRIDdb에서 0/1입니다. 릴리스된 pycubrid 1.7.1은 null_ok를 반대로 보고하고(cubrid-lab/pycubrid#431) 컬렉션 컬럼을 요소 타입 코드로 보고합니다(cubrid-lab/pycubrid#430). 두 문제 모두 pycubrid 1.8.0에서 수정되었으며, 계약 테스트는 수정된 동작을 요구합니다. SQLAlchemy 자체는 null_ok를 사용하지 않으며, 리플렉션(Inspector.get_columns())은 카탈로그에서 NULL 허용 여부를 읽으므로 영향을 받지 않습니다.

CUBRID 전용 기능

이 타입과 역량은 CUBRID 방언에 고유하며 MySQL, PostgreSQL, SQLite에 직접 대응물이 없습니다.

기능 설명
MONETARY 타입 통화 인식 서버 타입; 선언/컴파일 가능하지만 자동 리플렉트 안 됨
STRING 타입 VARCHAR(1,073,741,823)의 별칭 — 최대 길이 가변 문자열
OBJECT 타입 객체 식별자로 다른 행을 가리키는 OID 참조 타입
SET 컬렉션 유일한 원소의 순서 없는 컬렉션
MULTISET 컬렉션 중복을 허용하는 순서 없는 컬렉션
SEQUENCE 컬렉션 중복을 허용하는 순서 있는 컬렉션
3가지 MVCC 격리 수준 READ COMMITTED(기본), REPEATABLE READ, SERIALIZABLE
254자 식별자 MySQL(64)과 PostgreSQL(63)보다 김

컬렉션 타입

CUBRID의 컬렉션 타입(SET, MULTISET, SEQUENCE)은 지정된 데이터 타입의 원소를 담는 타입 컨테이너입니다. DDL에서 다음과 같이 선언됩니다:

SET(INTEGER)
MULTISET(VARCHAR)
SEQUENCE(DOUBLE)

이것들은 방언의 타입 컴파일러가 완전히 지원하며 Column 정의에서 사용할 수 있습니다.


CUBRID 전용 DML 구성

방언은 표준 SA API를 넘어서는 CUBRID 전용 DML 기능을 위한 커스텀 SQLAlchemy 구성을 제공합니다.

ON DUPLICATE KEY UPDATE

CUBRID는 INSERT … ON DUPLICATE KEY UPDATE를 지원합니다. 방언은 stmt.inserted 참조를 INSERT 바인드 파라미터를 다시 내보내는 방식으로 처리합니다(CUBRID는 VALUES() 함수를 지원하지 않습니다).

from sqlalchemy_cubrid import insert

stmt = insert(users).values(id=1, name="alice", email="alice@example.com")
stmt = stmt.on_duplicate_key_update(name="updated_alice")
# INSERT INTO users (id, name, email) VALUES (1, 'alice', 'alice@example.com')
# ON DUPLICATE KEY UPDATE name = 'updated_alice'

# 삽입되는 값 참조:
stmt = insert(users).values(id=1, name="alice", email="alice@example.com")
stmt = stmt.on_duplicate_key_update(name=stmt.inserted.name)
# ON DUPLICATE KEY UPDATE name = ?

허용되는 인자 형태: - 키워드 인자: stmt.on_duplicate_key_update(name="value") - 딕셔너리: stmt.on_duplicate_key_update({"name": "value"}) - 튜플 리스트 (순서 있음): stmt.on_duplicate_key_update([("name", "value"), ("email", "value")])

MERGE 문

CUBRID는 단일 연산으로 조건부 INSERT/UPDATE를 수행하는 SQL MERGE 문을 지원합니다.

from sqlalchemy_cubrid.dml import merge

stmt = (
    merge(target_table)
    .using(source_table)
    .on(target_table.c.id == source_table.c.id)
    .when_matched_then_update(
        {"name": source_table.c.name, "email": source_table.c.email},
        where=source_table.c.name.is_not(None),       # 선택적 WHERE
        delete_where=target_table.c.active == False,   # 선택적 DELETE WHERE
    )
    .when_not_matched_then_insert(
        {
            "id": source_table.c.id,
            "name": source_table.c.name,
            "email": source_table.c.email,
        },
        where=source_table.c.name.is_not(None),  # 선택적 WHERE
    )
)

생성 SQL:

MERGE INTO target_table
USING source_table
ON (target_table.id = source_table.id)
WHEN MATCHED THEN UPDATE SET name = source_table.name, email = source_table.email
  WHERE source_table.name IS NOT NULL
  DELETE WHERE target_table.active = 0
WHEN NOT MATCHED THEN INSERT (id, name, email)
  VALUES (source_table.id, source_table.name, source_table.email)
  WHERE source_table.name IS NOT NULL

빌더 메서드: - merge(target) — 팩토리 함수, 대상 테이블 설정 - .using(source) — 소스 테이블 또는 서브쿼리 - .on(condition) — 조인 조건 - .when_matched_then_update(values, where=None, delete_where=None) — UPDATE 절 - .when_matched_then_delete(where=None) — 기존 WHEN MATCHED 절에 DELETE WHERE 추가 - .when_not_matched_then_insert(values, where=None) — INSERT 절

when_matched_then_update 또는 when_not_matched_then_insert 중 최소 하나는 지정해야 합니다.

GROUP_CONCAT

CUBRID는 집계 함수로 GROUP_CONCAT을 지원합니다:

import sqlalchemy as sa

stmt = sa.select(sa.func.group_concat(users.c.name))
# SELECT GROUP_CONCAT(users.name) FROM users

stmt = sa.select(sa.func.group_concat(users.c.name.distinct()))
# SELECT GROUP_CONCAT(DISTINCT users.name) FROM users

# SQLAlchemy 2.1: aggregate_order_by()로 ORDER BY 지정
stmt = sa.select(sa.func.group_concat(users.c.name).aggregate_order_by(users.c.name.desc()))
# SELECT GROUP_CONCAT(users.name ORDER BY users.name DESC) FROM users

# SEPARATOR(SQLAlchemy 2.0에서는 ORDER BY도)에는 SQLAlchemy 구성 요소가 없으므로 텍스트로 작성합니다
stmt = sa.select(
    sa.func.group_concat(sa.literal_column("name ORDER BY name DESC SEPARATOR ';'"))
).select_from(users)
# SELECT GROUP_CONCAT(name ORDER BY name DESC SEPARATOR ';') FROM users

stmt = sa.select(sa.func.group_concat(sa.and_(users.c.id > 1, users.c.name != "")))
# SELECT GROUP_CONCAT((users.id > ? AND users.name != ?)) FROM users

NULL 값은 건너뛰며, NULL만 있는 그룹은 NULL을 반환합니다. ORDER BY가 없으면 연결되는 값의 순서는 정해져 있지 않습니다. 불리언 EXISTS, NOT, AND, OR 인자나 aggregate_order_by() 항목은 다른 함수 인자처럼 괄호로 묶입니다. CUBRID는 ORDER BY 항목을 하나만 받으며, 그 항목은 GROUP_CONCAT 인자 자체이거나 literal_column("1") 같은 위치 번호여야 합니다. 다른 식은 서버에서 실패합니다(-494). CUBRIDdb에서는 바인드 파라미터가 들어 있는 인자와 ORDER BY 항목도 실패합니다(-494). 드라이버 호환성을 참고하십시오. CUBRID 11.4에서 검증했으며 10.2, 11.0, 11.2에서는 검증하지 않았습니다. 지원하지 않는 형식:

  • sa.func.group_concat(column, sa.literal_column("SEPARATOR ','"))는 GROUP_CONCAT(users.name, SEPARATOR ',')로 렌더링되며 CUBRID가 거부합니다(-493). SEPARATOR는 식과 같은 literal_column() 텍스트에 넣으세요.
  • 두 개 이상의 인자: CUBRID는 하나의 식만 받습니다("invalid use of group_concat").
  • 두 개 이상의 aggregate_order_by() 항목(-493), 또는 SEPARATOR가 들어 있는 literal_column() 인자와 함께 쓴 aggregate_order_by()(ORDER BY가 그 뒤에 붙습니다, -493). 이 경우 ORDER BY ... SEPARATOR 전체를 텍스트로 작성하세요.
  • GROUP_CONCAT 이외의 함수(func.aggregate_strings() 포함)에 쓴 aggregate_order_by(): CUBRID의 다른 집계 함수는 ORDER BY를 받지 않으므로, ORDER BY를 버리지 않고 CompileError를 발생시킵니다.
  • .filter(...)를 붙인 함수(GROUP_CONCAT 포함)에 쓴 aggregate_order_by(): CUBRID에는 FILTER (WHERE ...) 절이 없으므로 CompileError를 발생시킵니다.

REPLACE INTO

CUBRID는 새 행을 삽입하거나, 중복 키가 발견되면 충돌하는 행을 삭제하고 새 행을 삽입하는 REPLACE INTO를 지원합니다.

from sqlalchemy_cubrid import replace

stmt = replace(users).values(id=1, name="alice", email="alice@example.com")
# REPLACE INTO users (id, name, email) VALUES (1, 'alice', 'alice@example.com')

replace() 구성은 insert()처럼 동작하지만 INSERT INTO 대신 REPLACE INTO를 생성합니다.

쿼리 추적

CUBRID는 표준 EXPLAIN 대신 SET TRACE ON / SHOW TRACE를 사용합니다. 방언은 trace_query() 유틸리티를 제공합니다:

from sqlalchemy_cubrid import trace_query

with engine.connect() as conn:
    traces = trace_query(conn, text("SELECT * FROM users WHERE id = 1"))
    for line in traces:
        print(line)

trace_query()는 전체 수명 주기를 처리합니다: 추적 활성화, 문장 실행, 추적 출력 수집, 추적 비활성화 — 전부 안전한 try/finally 블록 안에서.


인덱스 힌트

CUBRID는 SELECT 쿼리에서 인덱스 힌트를 지원합니다. SQLAlchemy 내장 힌트 메커니즘을 통해 사용할 수 있습니다 — 커스텀 방언 구성이 필요 없습니다.

USING INDEX

# Select.with_hint() 사용
stmt = (
    sa.select(users)
    .with_hint(users, "USING INDEX idx_users_name", dialect_name="cubrid")
)

# Select.suffix_with() 사용
stmt = sa.select(users).suffix_with("USING INDEX idx_users_name")

USE INDEX / FORCE INDEX / IGNORE INDEX

stmt = (
    sa.select(users)
    .with_hint(users, "USE INDEX (idx_users_name)", dialect_name="cubrid")
)

stmt = (
    sa.select(users)
    .with_hint(users, "FORCE INDEX (idx_users_email)", dialect_name="cubrid")
)

stmt = (
    sa.select(users)
    .with_hint(users, "IGNORE INDEX (idx_users_old)", dialect_name="cubrid")
)

참고: with_hint(dialect_name="cubrid")를 사용하면 힌트는 CUBRID 방언으로 컴파일할 때만 출력됩니다. 다른 방언은 이를 무시하므로 코드가 안전하게 이식 가능합니다.


알려진 한계와 로드맵

현재 지원되지 않지만 CUBRID 데이터베이스 발전과 커뮤니티 기여에 따라 향후 릴리스에 추가될 수 있는 기능들.

기능 상태 이유
RETURNING 절 ❌ CUBRID는 INSERT/UPDATE/DELETE … RETURNING 미지원
JSON 타입 ✅ JSON_EXTRACT를 통한 경로 표현식을 갖춘 네이티브 JSON 지원 (CUBRID 10.2+)
임시 테이블 ❌ CUBRID는 CREATE TEMPORARY TABLE 미지원
다중 스키마 ❌ CUBRID는 단일 스키마 모델로 동작
CHECK 제약 리플렉션 ❌ CUBRID는 CHECK 제약을 파싱하지만 무시
시퀀스 ❌ CUBRID는 대신 AUTO_INCREMENT 사용
Lateral 조인 ❌ LATERAL 키워드가 CUBRID에서 구문 오류 발생
전문 검색 ❌ MATCH … AGAINST 구문이나 전문 인덱스 없음
표준 EXPLAIN ❌ CUBRID는 대신 SET TRACE ON / SHOW TRACE 사용 (trace_query()로 지원)
Alembic 마이그레이션 ✅ CubridImpl 엔트리 포인트로 지원 (pip install sqlalchemy-cubrid[alembic])

최종 갱신: 2026년 9월 · sqlalchemy-cubrid v1.8.0 · SQLAlchemy 2.0–2.1