기능 지원 비교 (한국어)¶
🌐 FEATURE_SUPPORT.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.
sqlalchemy-cubrid의 역량을 MySQL, PostgreSQL, SQLite의 성숙한 SQLAlchemy 방언과 종합 비교합니다.
이 문서를 사용해 CUBRID 방언이 무엇을 지원하고 지원하지 않는지, 프로젝트의 방언 선택 시 다른 데이터베이스 백엔드와 어떻게 비교되는지 이해하세요.
목차¶
- 범례
- 요약
- DML — 데이터 조작 언어
- DDL — 데이터 정의 언어
- 쿼리 기능
- 타입 시스템
- 스키마 리플렉션
- 트랜잭션과 연결
- 방언 엔진 기능
- CUBRID 전용 기능
- CUBRID 전용 DML 구성
- 인덱스 힌트
- 알려진 한계와 로드맵
범례¶
| 기호 | 의미 |
|---|---|
| ✅ | 완전 지원 |
| ⚠️ | 부분 또는 에뮬레이트 지원 |
| ❌ | 미지원 |
요약¶
기능 범주별 상위 개요.
| 범주 | 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(예: Booleancol.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,SetColumnCommentDDL 구성을 구현합니다. 코멘트 리플렉션은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()이 컬럼을 감싸는 BooleanTypeDecorator,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"), JSONtrue/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는 CUBRIDBLOB/CLOBDDL로 컴파일되고 리플렉션되며,bytes/str을 바인딩하면 전체 값이 저장되고NULL은None으로 왕복합니다. 부분 지원인 부분은 일반 Core 또는 ORMSELECT가 반환하는 값입니다. 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(CUBRIDSTRING)에 저장하세요. 드라이버 호환성, 알려진 문제 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 코드입니다(예:VARCHAR2,NUMERIC7,INTEGER8,DOUBLE12,DATE13,TIMESTAMP15,BIGINT21). 동기와 비동기 pycubrid는 같은 값을 보고합니다. 드라이버 차이: 컬렉션 컬럼은 pycubrid에서SET16 /MULTISET17 /SEQUENCE18이지만 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에서 다음과 같이 선언됩니다:
이것들은 방언의 타입 컴파일러가 완전히 지원하며 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