Skip to content

문제 해결 가이드 (한국어)

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

sqlalchemy-cubrid의 흔한 문제에 대한 종합 해결책 — 연결 설정, SQL 컴파일, 타입 매핑, 스키마 리플렉션, Alembic 마이그레이션, ORM 패턴, 성능 튜닝.


목차


설치 문제

ImportError: Could not import CUBRIDdb

증상:

ImportError: Could not import CUBRIDdb. The cubrid:// and cubrid+cubriddb:// URLs use the CUBRIDdb C-extension driver. Install it with pip install "sqlalchemy-cubrid[cubriddb]" (the CUBRID-Python distribution on PyPI), or build CUBRIDdb from the official CUBRID/cubrid-python source. Alternatively, use the pure-Python pycubrid driver with pip install "sqlalchemy-cubrid[pycubrid]" and a cubrid+pycubrid:// URL. Check driver and server version compatibility separately.

원인: CUBRID C 확장 Python 드라이버가 설치되지 않음.

해결 — 옵션 A: CUBRIDdb C 확장 드라이버 설치: PyPI의 CUBRID-Python 패키지를 설치 extra로 설치하거나 공식 CUBRID/cubrid-python 소스에서 드라이버를 빌드하세요:

pip install "sqlalchemy-cubrid[cubriddb]"

소스 빌드에는 CUBRID CCI 헤더와 C 컴파일러가 필요합니다. 소스에서 CUBRIDdb 빌드를 참고하세요. 드라이버, Python, CUBRID 서버 버전의 호환성을 각각 확인하세요. 설치 방법을 지원한다는 사실이 모든 버전 조합의 테스트 완료를 의미하지는 않습니다.

해결 — 옵션 B: 순수 Python 드라이버 사용 (권장):

pip install "sqlalchemy-cubrid[pycubrid]"

그리고 연결 URL을 변경하세요:

# 이전 (C 확장)
engine = create_engine("cubrid://dba@localhost:33000/testdb")

# 이후 (순수 Python 드라이버 — CUBRID 네이티브 라이브러리 불필요)
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")

ModuleNotFoundError: No module named 'pycubrid'

증상:

ModuleNotFoundError: No module named 'pycubrid'

해결:

pip install pycubrid
# 또는 둘 다 함께 설치
pip install "sqlalchemy-cubrid[pycubrid]"

C 확장 빌드 실패

증상: CUBRIDdb(cubrid-python) 빌드가 컴파일 오류로 실패.

흔한 원인: - C 컴파일러 누락 (gcc / cl.exe) - CUBRID CCI 헤더 누락 - 호환되지 않는 플랫폼

해결: 대신 순수 Python pycubrid 드라이버를 사용하세요. CUBRID 네이티브 라이브러리가 필요 없습니다. [pycubrid] extra는 greenlet도 설치하므로 호환 wheel이 없으면 빌드 도구가 필요할 수 있습니다:

pip install "sqlalchemy-cubrid[pycubrid]"

비동기 엔진에서 greenlet 누락

증상: 비동기 SQLAlchemy 엔진 생성 시 연결 전에 다음 오류가 발생합니다.

ImportError: The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed.

원인: SQLAlchemy 2.1에서는 기본 패키지만 설치할 때 greenlet이 자동 설치되지 않을 수 있습니다. pycubrid만 설치하고 SQLAlchemy의 비동기 의존성을 설치하지 않았다면 create_async_engine()이 실패합니다.

해결: 비동기 의존성이 포함된 권장 방언 extra 또는 SQLAlchemy의 asyncio extra를 설치합니다.

python -m pip install "sqlalchemy-cubrid[pycubrid]"
# 방언과 드라이버가 이미 설치된 경우:
python -m pip install "sqlalchemy[asyncio]"

해당 Python·플랫폼에 맞는 greenlet wheel이 없으면 컴파일러 등 빌드 도구가 필요할 수 있습니다. 애플리케이션과 동일한 가상 환경에 설치되었는지 확인하세요. 비동기 방언 URL은 cubrid+aiopycubrid://를 사용하며 cubrid://가 아닙니다.


연결 문제

포트 33000 연결 거부

증상:

OperationalError: (CUBRIDdb.DatabaseError) Connection refused
OperationalError: (pycubrid.exceptions.OperationalError) failed to connect to CUBRID broker

해결:

  1. 컨테이너 내부에서 브로커와 DB 로그인을 확인하세요(호스트에 CUBRID가 설치되지 않았다면 호스트에서 cubrid 명령을 실행할 수 없음):
docker compose ps
docker compose exec -T cubrid cubrid broker status
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"
  1. 고정 시간 대기 대신 실제 DB 연결 준비 여부를 확인하세요:
docker compose up -d
docker compose ps
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"

레슨 1의 docker run 실습 컨테이너를 사용한다면 Compose 명령 대신:

docker ps --filter name=sqlalchemy-cubrid-quickstart
docker logs sqlalchemy-cubrid-quickstart
docker exec sqlalchemy-cubrid-quickstart cubrid broker status
docker exec sqlalchemy-cubrid-quickstart csql -u dba testdb -c "SELECT 1"

아직 SELECT 1이 실패하면 레슨 1의 최대 3분 준비 확인 루프를 사용하세요. 실행 중인 컨테이너나 브로커 상태만으로 DB 로그인 준비가 확인되는 것은 아닙니다.

  1. 잘못된 포트: 실제 브로커 포트는 cubrid_broker.conf에서 확인.

  2. 방화벽: 포트 33000이 차단되지 않았는지 확인.


컨테이너를 다시 만든 뒤 데이터베이스 서버 호스트를 찾을 수 없음

증상: 이 저장소의 docker-compose.yml로 docker compose down 후 up을 하면 브로커는 응답하지만 모든 연결이 실패하고, 예전 컨테이너 ID처럼 보이는 호스트를 가리킵니다:

OperationalError: ... Failed to connect to database server, 'testdb', on the following host(s): 43134de458f9

컨테이너 안에서 csql -u dba testdb -c "SELECT 1"도 같은 이유로 실패합니다. cubrid server status에는 testdb가 보여도 마찬가지입니다.

원인: 이미지는 databases.txt에 데이터베이스를 CUBRID_DB_HOST로, 이 값이 없으면 데이터베이스를 만든 컨테이너의 호스트 이름(그 컨테이너의 ID)으로 등록합니다. 이 파일은 cubrid-data 볼륨에, 11.4에서는 cubrid-databases 볼륨에 있습니다. down은 볼륨을 남기고, 다시 만든 컨테이너는 새 ID를 가지므로 기록된 호스트를 아무도 해석하지 못합니다. docker-compose.yml은 CUBRID_DB_HOST: localhost를 설정하므로 이 파일로 만든 볼륨은 계속 동작합니다. 이전 파일로 만든 볼륨에는 예전 ID가 남아 있습니다. make integration은 자체 Compose 프로젝트를 쓰고 down -v로 지우므로 영향이 없습니다.

데이터를 유지하는 해결: 컨테이너가 실행 중일 때 testdb의 호스트 필드만 localhost로 바꿉니다:

make docker-up
make docker-fix-db-host

이전 파일은 같은 위치에 databases.txt.before-fix-db-host로 저장됩니다.

데이터를 삭제하는 해결: make docker-clean은 docker compose down -v를 실행하며, 볼륨과 그 안의 모든 테이블을 삭제합니다. 다음 make docker-up이 새 데이터베이스를 만듭니다. 데이터가 필요 없는 볼륨에만 사용하세요.


인증 실패

증상: 사용한 드라이버와 SQLAlchemy 예외 래핑 방식에 따라 기반 CUBRID 오류 메시지에 다음과 같은 문구가 나타납니다:

User "..." is invalid
Incorrect or missing password

해결: URL의 사용자, 데이터베이스 및 비밀번호를 확인하세요. 비밀번호가 없는 dba 계정은 로컬 일회용 Docker 실습 환경의 설정이며 운영 서버에도 적용된다고 가정하면 안 됩니다:

# 로컬 일회용 실습 환경에서만 비밀번호 없음
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")

# 서버에 비밀번호가 설정된 경우
engine = create_engine("cubrid+pycubrid://dba:mypassword@localhost:33000/testdb")

끊어진 연결 / 연결 해제

증상:

OperationalError: Connection is closed
OperationalError: broker is not available

원인: CUBRID 브로커에는 SESSION_TIMEOUT(기본 ~300초)이 있습니다. 유휴 풀링 연결이 서버 측에서 만료됩니다.

해결: 풀 설정 구성:

engine = create_engine(
    "cubrid://dba@localhost:33000/testdb",
    pool_pre_ping=True,  # 체크아웃 전 네이티브 드라이버 핑
    pool_recycle=240,     # SESSION_TIMEOUT(300초) 전에 재활용
)

cubrid+pycubrid://와 cubrid+aiopycubrid://의 경우 pool_pre_ping=True는 이제 SELECT 1을 발행하는 대신 pycubrid의 네이티브 CHECK_CAS 핑을 사용합니다.

자세한 권장사항은 연결 가이드 — 풀 튜닝을 참고하세요.


cub_server 재시작 또는 장애 후 오류

증상: cub_server가 중지, 재시작되거나 비정상 종료된 뒤 열린 트랜잭션의 문장이 실패합니다:

DatabaseError: (-111) Your transaction has been aborted by the system due to server failure or mode change.
DatabaseError: (-224) A database has not been restarted.

원인: 브로커의 CAS가 cub_server와의 세션을 잃었습니다. CAS는 클라이언트가 트랜잭션을 끝낸 뒤에만 다시 연결하므로, 그 전까지는 cub_server가 다시 올라와도 같은 연결의 모든 문장이 -224로 실패합니다. pool_pre_ping으로는 잡을 수 없습니다. 연결은 이미 체크아웃되어 있고, CAS는 여전히 핑에 응답하기 때문입니다.

동작: 방언은 브로커가 CAS를 리셋하는 코드를 두 드라이버 모두에서 연결 끊김으로 취급합니다: -111(ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED), -199(ER_NET_SERVER_CRASHED), -224(ER_OBJ_NO_CONNECT), -677(ER_BO_CONNECT_FAILED). SQLAlchemy는 연결을 무효화하고(exc.connection_invalidated가 True) 풀은 새 연결을 엽니다. cub_server가 내려가 있는 동안 재연결에 실패한 pycubrid 연결(CAS did not answer CHECK_CAS out of transaction and reconnecting failed)도 연결 끊김입니다. 1.8.0까지의 릴리스는 이 오류들을 분류하지 않아 망가진 연결이 풀에 남았습니다.

해결: 설정할 것은 없습니다. 롤백하고(with engine.connect()나 Session 블록을 벗어나면 롤백됩니다) cub_server가 다시 연결을 받으면 트랜잭션을 재시도하세요.


중단된 쿼리 (-4)

증상: 실행 중인 문장이 실패합니다. 정확한 문자열은 드라이버마다 다릅니다:

# CUBRIDdb
DatabaseError: (-4, 'ERROR: DBMS, -4, Has been interrupted.[CAS INFO-127.0.0.1:33000,1,44].')

# pycubrid 1.13.0 이상
OperationalError: Has been interrupted. (errno=-4, description='Interrupted', sqlstate='HY008')

# pycubrid 1.13.0 이전
OperationalError: Has been interrupted. (errno=-4, description='Communication error', sqlstate='08S01')

원인: 다른 세션이 이 문장에 KILL QUERY <tran_index>를 실행했습니다. -4는 서버의 ER_INTERRUPTED이며 연결은 계속 쓸 수 있습니다. pycubrid 1.13.0 이상은 이를 Interrupted, SQLSTATE HY008로 설명합니다. 이전 릴리스는 통신 오류(Communication error, 08S01)로 보고했는데, 이는 pycubrid의 라벨일 뿐 서버 코드의 의미가 아니었습니다.

동작: 이 오류는 연결 끊김이 아닙니다. SQLAlchemy는 연결을 유지합니다(exc.connection_invalidated가 False). 1.8.0까지의 릴리스는 CUBRIDdb의 -4를 연결 끊김으로 취급해 연결을 교체했습니다(#572).

해결: 문장을 끝까지 실행해야 한다면 롤백하고 재시도하세요.


커넥션 풀 고갈

증상:

TimeoutError: QueuePool limit of size 5 overflow 10 reached

원인: - 연결이 풀로 반환되지 않음 (close()나 컨텍스트 매니저 누락) - 애플리케이션 동시성에 비해 풀이 너무 작음

해결:

engine = create_engine(
    "cubrid://dba@localhost:33000/testdb",
    pool_size=10,
    max_overflow=20,
    pool_timeout=30,
)

# 연결 반환을 위해 항상 컨텍스트 매니저 사용
with engine.connect() as conn:
    result = conn.execute(text("SELECT 1"))

잘못된 URL 형식

증상:

ArgumentError: Could not parse rfc1738 URL
NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:pycubrid

올바른 URL 형식:

# C 확장 드라이버 (CUBRIDdb)
engine = create_engine("cubrid://dba@localhost:33000/testdb")
engine = create_engine("cubrid+cubrid://dba@localhost:33000/testdb")

# 순수 Python 드라이버 (pycubrid)
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")

흔한 실수:

# 잘못됨 — 'pycubrid://'은 유효한 스킴이 아님
engine = create_engine("pycubrid://dba@localhost:33000/testdb")

# 잘못됨 — 포트는 쿼리 파라미터가 아니라 URL에 있어야 함
engine = create_engine("cubrid://dba@localhost/testdb?port=33000")

SQL 컴파일 문제

미지원 RETURNING 절

증상:

CompileError: RETURNING is not supported by this dialect

원인: CUBRID는 INSERT ... RETURNING이나 UPDATE ... RETURNING을 지원하지 않습니다.

해결: 방언이 드라이버 연결의 get_last_insert_id()로 자동 처리합니다. SELECT LAST_INSERT_ID()를 사용할 수도 있습니다:

from sqlalchemy import insert, select, text

# 삽입 후 생성된 ID 얻기
with engine.begin() as conn:
    result = conn.execute(
        insert(users).values(name="Alice", email="alice@example.com")
    )
    new_id = result.inserted_primary_key[0]
    print(f"New user ID: {new_id}")

# 또는 LAST_INSERT_ID() 사용
with engine.begin() as conn:
    conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
    last_id = conn.execute(text("SELECT LAST_INSERT_ID()")).scalar()

ORM 패턴:

with Session(engine) as session:
    user = User(name="Alice", email="alice@example.com")
    session.add(user)
    session.flush()  # INSERT 전송, user.id 채움
    print(user.id)   # flush 후 사용 가능
    session.commit()

불리언 컬럼 동작

증상: 불리언 컬럼이 True/False 대신 0과 1을 저장/반환.

원인: CUBRID에는 네이티브 BOOLEAN 타입이 없습니다. 방언은 Boolean을 SMALLINT로 매핑합니다.

해결: 예상된 동작입니다. SQLAlchemy가 Python bool과 SMALLINT(0/1) 사이를 자동 변환합니다:

from sqlalchemy import Boolean
from sqlalchemy.orm import mapped_column, Mapped

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    # SMALLINT로 저장: True→1, False→0
    # 자동으로 bool로 조회됨

raw SQL에서:

# 삽입
conn.execute(text("INSERT INTO users (is_active) VALUES (:val)"), {"val": 1})

# 조회 — 0/1과 비교
conn.execute(text("SELECT * FROM users WHERE is_active = 1"))

LIMIT / OFFSET 구문

증상: 페이지네이션에서 예기치 않은 쿼리 동작.

방언은 MySQL 스타일 LIMIT [offset,] row_count를 냅니다. CUBRID는 표준 LIMIT row_count OFFSET offset 형태도 받아들이지만 방언이 이를 생성하지는 않습니다. OFFSET 키워드는 방언의 출력에 아예 나타나지 않습니다:

select(users).limit(3)              # → SELECT ... FROM users LIMIT 3
select(users).limit(10).offset(20)  # → SELECT ... FROM users LIMIT 20, 10
select(users).offset(5)             # → SELECT ... FROM users LIMIT 5, 4611686018427387904

쉼표 형태는 오프셋이 있을 때만 나오며, 이때 오프셋이 앞에 옵니다. LIMIT 20, 10은 20행을 건너뛰고 10행을 반환하며, 10행부터 20행으로 제한한다는 뜻이 아닙니다.

LIMIT 없는 OFFSET. CUBRID에는 단독 OFFSET이 없어서(SELECT ... OFFSET 5는 문법 오류입니다) .limit() 없이 .offset(n)만 준 경우에도 행 수를 반드시 명시해야 하며, 방언은 사실상 무제한에 해당하는 값을 채워 넣습니다 — 위 세 번째 줄입니다. 쿼리 로그의 두 번째 피연산자가 아주 큰 수인 것은 이 센티넬이며 버그가 아닙니다. 정확한 값과 부호 있는 BIGINT 최댓값을 의도적으로 피한 이유는 기능 지원을 참고하세요.


CAST 타입 제한

증상:

ProgrammingError: CAST to this type is not supported

원인: CUBRID의 CAST()는 제한된 대상 타입 집합을 지원합니다.

지원되는 CAST 대상:

대상 타입 예
CHAR(n) CAST(col AS CHAR(10))
VARCHAR(n) CAST(col AS VARCHAR(100))
NCHAR(n) CAST(col AS NCHAR(10))
INTEGER CAST(col AS INTEGER)
BIGINT CAST(col AS BIGINT)
FLOAT CAST(col AS FLOAT)
DOUBLE CAST(col AS DOUBLE)
NUMERIC(p,s) CAST(col AS NUMERIC(10,2))
DATE CAST(col AS DATE)
TIME CAST(col AS TIME)
DATETIME CAST(col AS DATETIME)
TIMESTAMP CAST(col AS TIMESTAMP)

미지원: CAST(... AS BOOLEAN), CAST(... AS BLOB), CAST(... AS SET).


예약어 충돌

증상:

ProgrammingError: Syntax error near 'value'

원인: CUBRID에는 컬럼/테이블 이름과 충돌할 수 있는 많은 예약어가 있습니다.

흔한 CUBRID 예약어:

예약어 안전한 대안
value val, item_value
count cnt, item_count
data file_data, raw_data
level user_level
action user_action
status item_status
type item_type

해결 — 방언이 식별자를 자동 인용하지만, raw SQL에 text()를 쓴다면 수동으로 인용하세요:

# ORM과 Core 구성에는 자동 인용이 동작
class Config(Base):
    __tablename__ = "config"
    value: Mapped[str] = mapped_column("val", String(100))  # 컬럼 이름 변경

# raw SQL에는 큰따옴표 사용
conn.execute(text('SELECT "value" FROM config'))

CubridIdentifierPreparer가 예약어 인용을 자동 처리합니다. 전체 예약어 목록은 base.py에 유지됩니다.


JSON 타입 사용

상태: JSON은 sqlalchemy-cubrid v1.2.0부터 지원되며 CUBRID ≥ 10.2가 필요합니다.

기본 사용:

from sqlalchemy import Column, MetaData, Table, select
from sqlalchemy_cubrid import JSON

metadata = MetaData()

config = Table(
    "config",
    metadata,
    Column("data", JSON),
)

경로 접근: col["key"]는 CUBRID에서 JSON_EXTRACT(...)로 컴파일됩니다.

stmt = select(config.c.data["theme"])

이 오류가 보이면:

CompileError: JSON is not supported by this dialect

sqlalchemy-cubrid>=1.2.0으로 업그레이드하세요.

CUBRID < 10.2 회피: JSON을 VARCHAR나 STRING으로 저장하고 Python에서 직렬화/역직렬화하세요:

import json
from sqlalchemy import String, TypeDecorator

class JSONType(TypeDecorator):
    """Store JSON as VARCHAR in CUBRID."""
    impl = String(4096)
    cache_ok = True

    def process_bind_param(self, value, dialect):
        if value is not None:
            return json.dumps(value)
        return None

    def process_result_value(self, value, dialect):
        if value is not None:
            return json.loads(value)
        return None

class Config(Base):
    __tablename__ = "config"
    id: Mapped[int] = mapped_column(primary_key=True)
    settings: Mapped[dict] = mapped_column(JSONType)

타입 매핑 문제

불리언이 SMALLINT로 매핑

위의 불리언 컬럼 동작을 참고하세요.


Text가 STRING으로 매핑

동작: SQLAlchemy의 Text 타입은 CUBRID의 STRING 타입으로 매핑되며, VARCHAR(1,073,741,823)과 동등합니다 — 매우 큰 가변 길이 문자열.

from sqlalchemy import Text

class Article(Base):
    __tablename__ = "articles"
    content: Mapped[str] = mapped_column(Text)
    # DDL에서 → STRING (VARCHAR(1073741823)과 동등)

이것은 올바른 동작입니다. CUBRID의 STRING 타입은 MySQL/PostgreSQL의 TEXT와 같은 목적을 수행합니다.


ARRAY 타입 없음

원인: CUBRID에는 표준 ARRAY 타입이 없습니다. 대신 컬렉션 타입을 제공합니다: SET, MULTISET, SEQUENCE.

from sqlalchemy_cubrid import SET, MULTISET, SEQUENCE

class Product(Base):
    __tablename__ = "products"
    id: Mapped[int] = mapped_column(primary_key=True)
    tags: Mapped[str] = mapped_column(SET(String(50)))        # 유일, 순서 없음
    categories: Mapped[str] = mapped_column(MULTISET(String(50)))  # 중복 허용
    colors: Mapped[str] = mapped_column(SEQUENCE(String(50)))      # 순서 있음

SQL 대응물:

CREATE TABLE products (
    id INTEGER AUTO_INCREMENT PRIMARY KEY,
    tags SET(VARCHAR(50)),
    categories MULTISET(VARCHAR(50)),
    colors SEQUENCE(VARCHAR(50))
);

LOB 컬럼 동작

CLOB(Character Large Object)과 BLOB(Binary Large Object) 타입이 지원됩니다:

from sqlalchemy import LargeBinary, Text

class Document(Base):
    __tablename__ = "documents"
    content: Mapped[str] = mapped_column(Text)       # STRING 사용 (CLOB 유사)
    binary_data: Mapped[bytes] = mapped_column(LargeBinary)  # BLOB 사용

참고: LOB 동작은 드라이버에 따라 다릅니다: - CUBRIDdb (C 확장): LOB 컬럼이 raw 바이트나 LOB 핸들을 반환할 수 있음 - pycubrid (순수 Python): LOB 컬럼이 메타데이터 딕셔너리 반환 (lob_type, lob_length, file_locator, packed_lob_handle)

간단한 사용 사례에는 문자열/바이트를 직접 삽입하면 LOB 컬럼에 저장됩니다.


컬렉션 타입 (SET, MULTISET, SEQUENCE)

리플렉션: 컬렉션 컬럼이 있는 테이블을 리플렉트하면 방언이 적절한 SQLAlchemy 타입으로 되돌려 매핑합니다:

from sqlalchemy import inspect

inspector = inspect(engine)
columns = inspector.get_columns("products")
for col in columns:
    print(f"{col['name']}: {col['type']}")
    # tags: SET(VARCHAR(50))
    # categories: MULTISET(VARCHAR(50))

컬렉션 데이터 삽입: raw SQL에서 CUBRID의 컬렉션 리터럴 구문을 사용하세요:

INSERT INTO products (tags) VALUES ({'red', 'blue', 'green'});

Decimal 정밀도

증상: Decimal 값이 정밀도를 잃음.

해결: 정확한 십진 연산을 위해 NUMERIC(precision, scale)을 사용하세요:

from sqlalchemy_cubrid import NUMERIC

class Product(Base):
    __tablename__ = "products"
    price: Mapped[Decimal] = mapped_column(NUMERIC(10, 2))
    # 소수점 이하 2자리로 정확히 10자리 저장

CUBRID는 최대 38자리 정밀도를 지원합니다.


스키마 리플렉션 문제

리플렉션 중 테이블을 찾을 수 없음

증상:

NoSuchTableError: table_name

원인:

  1. 테이블이 존재하지 않음 — CUBRID에서 직접 확인
  2. 대소문자 구분 — CUBRID는 식별자를 (SQL 표준이 대문자로 폴딩하는 것과 달리) 소문자로 폴딩합니다:
# CUBRID는 테이블 이름을 소문자로 저장
inspector = inspect(engine)
tables = inspector.get_table_names()
print(tables)  # ['users', 'products'] — 전부 소문자
  1. 잘못된 데이터베이스 — 연결 URL이 올바른 데이터베이스를 가리키는지 확인

참고: 리플렉션은 객체가 없을 때만 NoSuchTableError를 발생시킵니다: db_class 조회에서 해당 테이블이나 뷰를 찾지 못하거나, 서버가 Unknown class "<owner>.<name>"을 보고하거나, 존재하는 객체라면 항상 행을 반환하는 쿼리(SHOW CREATE TABLE, SHOW CREATE VIEW)가 행을 반환하지 않는 경우입니다 (#589). 구문 오류나 그 밖의 리플렉션 쿼리 실패는 원래 예외(예: sqlalchemy.exc.ProgrammingError)로 그대로 전파됩니다. CUBRID는 두 경우 모두 네이티브 오류 -493을 사용하며, 1.8.0 이전 pycubrid는 모든 -493 오류에 SQLSTATE 42S02(Table not found)를 보고합니다 (#454).

참고: get_foreign_keys()와 get_unique_constraints()는 더 이상 실패한 SHOW CREATE TABLE을 빈 리스트로 바꾸지 않습니다 (#589). 리플렉션 중 연결 끊김, 권한 오류, 드라이버 오류가 발생하면 예외가 전파되므로, Alembic autogenerate는 이미 존재하는 외래 키에 대해 add_fk를 제안하는 대신 그 오류로 중단됩니다. 새 연결로 다시 시도하십시오(pool_pre_ping=True는 오래된 풀 연결을 교체합니다).


테이블 이름의 대소문자 구분

CUBRID는 인용되지 않은 식별자를 소문자로 폴딩합니다. 이것은 PostgreSQL(소문자)과 Oracle(대문자)과 다릅니다.

# 전부 같은 테이블을 가리킴
conn.execute(text("CREATE TABLE MyTable (id INT)"))  # 'mytable'로 저장
conn.execute(text("SELECT * FROM MYTABLE"))          # 'mytable' 발견
conn.execute(text("SELECT * FROM mytable"))          # 'mytable' 발견

# 대소문자를 보존하려면 큰따옴표 사용
conn.execute(text('CREATE TABLE "MyTable" (id INT)'))  # 'MyTable'로 저장
conn.execute(text('SELECT * FROM "MyTable"'))          # 반드시 인용 필요

방언의 CubridIdentifierPreparer는 initial_quote = '"'로 대소문자를 구분하는 이름을 인용합니다. CUBRID는 인용되지 않은 이름을 이미 소문자로 폴딩하며 이는 SQLAlchemy의 규칙과 일치하므로, 방언은 requires_name_normalize = False로 설정합니다.


뷰 리플렉션

지원됩니다. inspector.get_view_names()를 사용하세요:

inspector = inspect(engine)
views = inspector.get_view_names()
print(views)  # ['my_view', 'active_users_view']

스키마 지원 없음

CUBRID는 단일 스키마 모델을 사용합니다. PostgreSQL과 달리 데이터베이스 내에 여러 스키마 개념이 없습니다.

# 잘못됨 — CUBRID는 schema 파라미터 미지원
inspector.get_table_names(schema="public")

# 올바름 — schema 생략
inspector.get_table_names()

코드가 여러 데이터베이스(PostgreSQL + CUBRID)에서 동작해야 한다면 조건문으로 처리하세요:

schema = None if dialect_name == "cubrid" else "public"
tables = inspector.get_table_names(schema=schema)

ORM 문제

autoincrement와 lastrowid

CUBRID는 자동 생성 기본 키에 (시퀀스가 아니라) AUTO_INCREMENT를 사용합니다:

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100))

삽입 후 ID는 flush()로 사용 가능합니다:

with Session(engine) as session:
    user = User(name="Alice")
    session.add(user)
    session.flush()
    print(user.id)  # 자동 생성 ID
    session.commit()

방언은 CubridExecutionContext에 get_lastrowid()를 구현하며, raw_conn.get_last_insert_id()를 호출합니다 (드라이버 메서드를 사용할 수 없으면 SQL의 SELECT LAST_INSERT_ID()로 폴백).


시퀀스 없음 — AUTO_INCREMENT 사용

증상:

CompileError: sequences are not supported by this dialect

원인: CUBRID는 SQL 시퀀스를 지원하지 않습니다. 대신 AUTO_INCREMENT 컬럼을 사용하세요:

# 잘못됨 — 시퀀스 미지원
from sqlalchemy import Sequence
id = Column(Integer, Sequence("user_id_seq"), primary_key=True)

# 올바름 — autoincrement 사용
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

릴레이션십 캐스케이드 동작

외래 키 캐스케이드는 CUBRID에서 정상 동작합니다:

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    posts: Mapped[list["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan")

class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
    author: Mapped["User"] = relationship(back_populates="posts")

대량 삽입 성능

많은 행을 삽입하려면, dict 리스트와 함께 insert().values()를 사용하세요:

from sqlalchemy import insert

with engine.begin() as conn:
    conn.execute(
        insert(users),
        [
            {"name": "Alice", "email": "alice@example.com"},
            {"name": "Bob", "email": "bob@example.com"},
            {"name": "Charlie", "email": "charlie@example.com"},
        ],
    )

매우 큰 데이터셋에는 executemany()로 배치 삽입하거나 executemany_batch()(pycubrid 전용)를 사용하세요.


DML 확장 문제

ON DUPLICATE KEY UPDATE 동작 안 함

증상: on_duplicate_key_update()가 AttributeError 발생.

원인: 방언의 커스텀 insert() 대신 SQLAlchemy 내장 insert()를 사용 중:

# 잘못됨 — 표준 SQLAlchemy insert에는 on_duplicate_key_update 없음
from sqlalchemy import insert
stmt = insert(users).values(name="Alice")
stmt = stmt.on_duplicate_key_update(name="Alice Updated")  # AttributeError!

# 올바름 — 방언의 insert 사용
from sqlalchemy_cubrid import insert
stmt = insert(users).values(name="Alice")
stmt = stmt.on_duplicate_key_update(name="Alice Updated")

중복 감지가 동작하려면 테이블에 UNIQUE 또는 PRIMARY KEY 제약이 있어야 합니다.


MERGE 문 오류

증상: MERGE 문 컴파일 실패.

체크리스트:

  1. 올바른 모듈에서 임포트:
from sqlalchemy_cubrid.dml import merge
  1. 필수 절이 모두 있어야 함:
stmt = (
    merge(target)
    .using(source)                        # 필수
    .on(target.c.id == source.c.id)       # 필수
    .when_matched_then_update(...)        # WHEN 절 최소 하나
    .when_not_matched_then_insert(...)
)
  1. when_matched_then_delete()는 선행 when_matched_then_update() 필요:
# 잘못됨 — 갱신 없는 삭제
merge(t).using(s).on(condition).when_matched_then_delete()

# 올바름 — 갱신 후 삭제
merge(t).using(s).on(condition).when_matched_then_update({...}).when_matched_then_delete(where=...)

REPLACE INTO 동작

REPLACE INTO는 기존 행을 삭제하고 새 행을 삽입합니다 (제자리 갱신하는 ON DUPLICATE KEY UPDATE와 달리):

from sqlalchemy_cubrid import replace

# 이것은 충돌하는 키의 기존 행을 삭제하고,
# 새 행을 삽입함
stmt = replace(users).values(id=1, name="Alice", email="alice@new.com")

경고: 테이블이 AUTO_INCREMENT를 사용하면 REPLACE INTO는 행에 새 auto-increment ID를 부여합니다. 기존 행의 ID를 보존하려면 대신 ON DUPLICATE KEY UPDATE를 사용하세요.


Alembic 마이그레이션 문제

방언 'cubrid'에 대한 구현을 찾을 수 없음

증상:

KeyError: 'cubrid'   (raised from alembic/ddl/impl.py, DefaultImpl.get_by_dialect)

해결: alembic extra와 함께 설치:

pip install "sqlalchemy-cubrid[alembic]"

Alembic이 같은 환경에 설치되어 있으면 CubridImpl은 CUBRID 방언이 로드될 때 스스로 등록됩니다. 수동 구성이나 env.py 임포트가 필요 없습니다.


ALTER COLUMN TYPE 거부됨 (손실 변환)

증상:

Error: cannot coerce value ... (incompatible type change)

CUBRID는 네이티브 ALTER TABLE ... MODIFY로 컬럼 데이터 타입 변경을 지원하며, 방언이 자동으로 냅니다. 변경은 손실/호환 불가이고 alter_table_change_type_strict 시스템 파라미터가 yes일 때만 거부됩니다 (no이면 CUBRID가 조용히 절단할 수 있음).

정상 사용 (네이티브 MODIFY 출력):

def upgrade():
    op.alter_column("users", "name", type_=sa.String(500))

진짜 손실 변환에 대한 회피 — batch_alter_table 사용 (테이블 재생성):

def upgrade():
    with op.batch_alter_table("users") as batch_op:
        batch_op.alter_column("name", type_=sa.String(500))

def downgrade():
    with op.batch_alter_table("users") as batch_op:
        batch_op.alter_column("name", type_=sa.String(100))

batch_alter_table은 새 테이블 생성, 데이터 복사, 원본 삭제, 이름 변경을 수행합니다.


RENAME COLUMN

CUBRID는 ALTER TABLE ... RENAME COLUMN을 지원합니다. 방언이 네이티브로 출력하며, alter_column(new_column_name=...)은 더 이상 예외를 발생시키지 않습니다. 이름 변경 + 타입 변경 결합은 단일 CHANGE 문으로 출력됩니다.

정상 사용 (네이티브 RENAME COLUMN 출력):

def upgrade():
    op.alter_column("users", "old_name", new_column_name="new_name")

실패 후 부분 마이그레이션

증상: 마이그레이션이 중간에 실패해 데이터베이스가 불일치 상태로 남음.

배경: CUBRID의 DDL은 트랜잭션으로 처리됩니다. 방언이 항상 설정하듯 클라이언트 자동 커밋이 꺼져 있으면 CREATE, ALTER, DROP, TRUNCATE, CREATE INDEX, RENAME 등은 ROLLBACK으로 되돌려집니다. CubridImpl은 transactional_ddl = True를 설정하므로, 기본적으로 실패한 alembic upgrade는 alembic_version 갱신을 포함해 그 실행의 모든 리비전을 롤백합니다. transaction_per_migration=True이면 끝난 리비전은 커밋된 채 남고 실패한 리비전만 롤백됩니다.

원인: 실패 전에 무언가가 커밋했습니다:

  • 클라이언트 자동 커밋 — 드라이버 수준 자동 커밋, csql의 기본 자동 커밋 모드, 또는 방언이 지원하는 경우(#501) isolation_level="AUTOCOMMIT" — 은 문마다 커밋합니다
  • 오프라인(--sql) 스크립트를 csql --no-auto-commit --no-single-line 없이 실행했습니다. csql의 기본 단일 행 모드는 실패한 문 뒤에도 계속 실행하고, 마지막 COMMIT;까지 실행한 뒤 0으로 종료합니다
  • 리비전이 op.execute() 등으로 직접 COMMIT을 실행했습니다

예방:

  1. 클라이언트 자동 커밋 없이 마이그레이션을 실행하고, 오프라인 스크립트는 csql --no-auto-commit --no-single-line으로 실행(첫 오류에서 멈추고 1로 종료하며, 열린 트랜잭션은 롤백됨)
  2. 긴 마이그레이션이나 큰 테이블에는 transaction_per_migration=True 사용 — 커밋되지 않은 DDL은 스키마 잠금을 유지하므로 다른 세션이 트랜잭션이 끝날 때까지 기다립니다
  3. 먼저 스테이징 데이터베이스에서 마이그레이션 테스트
  4. 마이그레이션 실행 전 데이터베이스 백업

복구:

# 현재 상태 확인
alembic current

# 데이터베이스를 수동 수정한 뒤 올바른 리비전으로 스탬프
alembic stamp <revision_id>

Autogenerate가 변경을 감지하지 못함

가능한 원인:

  1. 모델이 임포트되지 않음 — Alembic autogenerate는 env.py 실행 시 임포트된 모델만 봄:
# env.py에서 — 모든 모델 임포트
from myapp.models import Base
target_metadata = Base.metadata
  1. 다른 스키마로 테이블이 이미 존재 — CUBRID 리플렉션이 모델 정의와 완벽히 일치하지 않을 수 있음 (예: VARCHAR(4096) vs String())

  2. 컬렉션 타입 — SET, MULTISET, SEQUENCE 타입의 변경은 autogenerate가 감지하지 못할 수 있음


격리 수준 문제

격리 수준 설정

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

# 엔진 수준 (모든 연결에 적용)
engine = create_engine(
    "cubrid://dba@localhost:33000/testdb",
    isolation_level="REPEATABLE READ",
)

# 연결 수준
with engine.connect().execution_options(
    isolation_level="SERIALIZABLE"
) as conn:
    result = conn.execute(text("SELECT * FROM accounts"))

사용 가능한 수준:

SQLAlchemy 이름 CUBRID 숫자 수준
"SERIALIZABLE" 6
"REPEATABLE READ" 5
"READ COMMITTED" (기본) 4
"AUTOCOMMIT" 드라이버 오토커밋 모드(오토커밋 충돌 참고)

레거시 pre-MVCC 수준(READ UNCOMMITTED와 숫자 코드 1–3으로 해석되던 세분화된 class/instance 조합)은 CUBRID 10.0에서 제거되었으며 더 이상 받지 않습니다. create_engine(isolation_level=...)이나 execution_options(isolation_level=...)에 전달하면 sqlalchemy.exc.ArgumentError가 발생합니다. 격리 수준을 참고하세요.


DDL은 현재 트랜잭션 안에서 실행됨

CUBRID는 DDL을 암묵적으로 커밋하지 않습니다. 클라이언트 자동 커밋이 꺼져 있으면(방언은 모든 연결에서 이를 끕니다) 격리 수준과 관계없이 DDL은 현재 트랜잭션의 일부입니다:

with engine.begin() as conn:
    conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
    conn.execute(text("CREATE TABLE temp (id INT)"))  # 아직 커밋되지 않음
    conn.execute(text("INSERT INTO users (name) VALUES ('Bob')"))
    raise RuntimeError("abort")  # 두 INSERT와 CREATE TABLE이 모두 롤백됨

자동 커밋이 일어나는 경우는 클라이언트 자동 커밋(CCI_DEFAULT_AUTOCOMMIT / 드라이버 수준 자동 커밋, csql의 기본 모드, 또는 방언이 지원하는 경우(#501) isolation_level="AUTOCOMMIT")뿐이며, 이때는 DDL이든 DML이든 문마다 커밋됩니다.

스키마 잠금에 주의: 커밋되지 않은 DDL은 트랜잭션이 끝날 때까지 테이블의 스키마 잠금을 유지하므로, 그 테이블을 사용하는 다른 세션은 기다립니다(SCH_S_LOCK 대기. CUBRID의 기본 lock_timeout은 무제한이므로, 잠금 타임아웃을 설정하지 않으면 트랜잭션이 끝날 때까지 기다립니다). DDL은 바로 커밋하고, 긴 DDL 트랜잭션은 바쁜 시간대를 피하세요.


트랜잭션 문제

데이터가 저장되지 않음

증상: 오류 없이 삽입됐는데 재연결 후 사라짐.

원인: commit() 누락 또는 트랜잭션 컨텍스트 미사용.

해결:

# 옵션 1: 명시적 커밋
with engine.connect() as conn:
    conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
    conn.commit()

# 옵션 2: begin()은 성공 시 자동 커밋
with engine.begin() as conn:
    conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
    # 성공적 종료 시 자동 커밋

# 옵션 3: ORM 세션
with Session(engine) as session:
    session.add(User(name="Alice"))
    session.commit()

오토커밋 충돌

증상: 문장이 예기치 않게 커밋됨.

배경: 두 CUBRID 드라이버 모두 기본적으로 autocommit=True이지만, 방언이 SQLAlchemy가 트랜잭션을 관리할 수 있도록 모든 새 연결에서 autocommit=False로 설정합니다.

진짜 오토커밋이 필요하면 (각 문장이 즉시 커밋) SQLAlchemy의 AUTOCOMMIT 격리 수준을 사용하세요. 세 드라이버(cubrid://, cubrid+pycubrid://, cubrid+aiopycubrid://) 모두에서 동작합니다:

with engine.connect().execution_options(
    isolation_level="AUTOCOMMIT"
) as conn:
    conn.execute(text("INSERT INTO logs (msg) VALUES ('event')"))
    # 즉시 커밋됨; conn.rollback()으로 되돌릴 수 없음

create_engine(..., isolation_level="AUTOCOMMIT")과 engine.execution_options(isolation_level="AUTOCOMMIT")도 동작합니다. execution_options()로 AUTOCOMMIT으로 바꾼 연결은 풀로 돌아가면 다시 트랜잭션 모드가 됩니다. #501 이전 릴리스는 AUTOCOMMIT에 대해 ArgumentError(execution_options) 또는 ValueError(create_engine)를 발생시켰습니다. 격리 수준을 참고하세요.


세이브포인트 한계

CUBRID는 SAVEPOINT와 ROLLBACK TO SAVEPOINT를 지원하지만, RELEASE SAVEPOINT는 지원하지 않습니다. 방언은 do_release_savepoint()를 no-op로 구현합니다.

with engine.begin() as conn:
    conn.execute(text("INSERT INTO users (name) VALUES ('Alice')"))
    savepoint = conn.begin_nested()
    try:
        conn.execute(text("INSERT INTO users (name) VALUES ('duplicate')"))
        savepoint.commit()
    except Exception:
        savepoint.rollback()
    # Alice의 삽입은 여전히 대기 중
    conn.commit()

성능 문제

느린 스키마 리플렉션

증상: inspector.get_table_names()나 metadata.reflect()가 느림.

원인: 스키마 리플렉션은 시스템 카탈로그를 조회하며, 테이블이 많으면 느릴 수 있습니다.

해결: 필요한 테이블만 리플렉트하세요:

# 느림 — 전체 테이블 리플렉트
metadata.reflect(bind=engine)

# 빠름 — 특정 테이블만 리플렉트
metadata.reflect(bind=engine, only=["users", "products", "orders"])

연결 오버헤드

각 연결은 다단계 CAS 핸드셰이크를 수행합니다. 커넥션 풀링을 사용하세요:

# 기본 풀 (웹 앱 권장)
engine = create_engine(
    "cubrid://dba@localhost:33000/testdb",
    pool_size=5,
    pool_pre_ping=True,
)

# 스크립트용 NullPool (풀링 없음)
from sqlalchemy.pool import NullPool
engine = create_engine(
    "cubrid://dba@localhost:33000/testdb",
    poolclass=NullPool,
)

문장 캐싱

방언은 SQLAlchemy의 문장 캐싱을 지원합니다 (supports_statement_cache = True). SQLAlchemy 2.0에서 기본 활성화되며 반복 쿼리의 컴파일 오버헤드를 크게 줄입니다.

구성이 필요 없습니다 — 자동으로 동작합니다.


Docker 문제

Apple silicon / arm64에서 Docker 이미지 실행: 실습에 사용하는 cubrid/cubrid:11.4 이미지는 amd64용으로 안내되어 있으며 arm64 네이티브 지원은 확인되지 않았습니다. Apple silicon Mac에서는 레슨 1의 docker run 명령에 --platform linux/amd64를 추가하여 Docker 에뮬레이션을 시도할 수 있습니다. 기존 최대 3분 DB 연결 준비 확인 루프와 로그 점검도 그대로 적용하세요. 이 방법은 실제 검증되지 않았고 속도가 느리거나 실행되지 않을 수 있습니다. 동작하지 않으면 기존에 실행 중인 CUBRID 서버를 사용하는 레슨 1 옵션 A로 진행하세요. 로컬 포트의 127.0.0.1 바인딩은 유지해야 합니다.

컨테이너는 시작되는데 연결 안 됨

해결:

# 1. 컨테이너가 실제로 실행 중인지 확인
docker compose ps

# 2. 고정 시간 대기 대신 Compose 헬스체크/실제 DB 로그인 확인
docker compose up -d
docker compose ps
docker compose exec -T cubrid csql -u dba testdb -c "SELECT 1"

# 레슨 1 실습 컨테이너를 사용한다면:
# docker logs sqlalchemy-cubrid-quickstart
# docker exec sqlalchemy-cubrid-quickstart csql -u dba testdb -c "SELECT 1"
# 먼저 레슨 1의 최대 3분 준비 확인 루프를 사용하세요.

# 3. 연결 테스트
python3 -c "
from sqlalchemy import create_engine, text
engine = create_engine('cubrid+pycubrid://dba@localhost:33000/testdb')
with engine.connect() as conn:
    print(conn.execute(text('SELECT 1')).scalar())
"

데이터베이스가 생성되지 않음

Docker 이미지는 CUBRID_DB에 지정된 데이터베이스만 생성합니다:

services:
  cubrid:
    image: cubrid/cubrid:11.4
    environment:
      CUBRID_DB: testdb  # 이 데이터베이스만 생성됨
    ports:
      - "127.0.0.1:33000:33000"

연결 URL이 다른 데이터베이스 이름을 사용하면 실패합니다. CUBRID_DB를 연결 URL과 일치시키세요.


버전별 동작

CUBRID 버전을 명시적으로 지정하세요:

# 기본 (11.2)
docker compose up -d

# 특정 버전
CUBRID_VERSION=11.4 docker compose up -d
CUBRID_VERSION=10.2 docker compose up -d

버전별 검증 현황:

기능 CUBRID 10.2 CUBRID 11.0–11.4
UPDATE의 LIMIT 검증되지 않음 검증되지 않음
CTE (WITH / WITH RECURSIVE) ✅ 검증됨 ✅ 검증됨
MERGE 버전별 검증 근거 없음 버전별 검증 근거 없음
인덱스 코멘트 검증되지 않음 검증되지 않음

WITH와 WITH RECURSIVE는 CUBRID 10.2, 11.0, 11.2, 11.4에서 실제로 검증되었습니다. CTE 검증 근거 및 지원 매트릭스를 참고하세요. “검증되지 않음”은 이 문서에 버전별 검증 근거가 없다는 뜻이며, 기능 미지원이라는 뜻이 아닙니다.


디버깅 기법

문제 해결 의사결정 흐름

flowchart TD
    start[Issue observed] --> kind{Primary symptom}
    kind -->|Import / Module error| install[Check driver install]
    kind -->|Connection failure| conn[Validate URL, host, port, credentials]
    kind -->|SQL compile/runtime error| sql[Inspect generated SQL and unsupported feature]
    kind -->|Migration failure| mig[Check Alembic limitations and client autocommit]
    kind -->|Performance issue| perf[Check pool settings, indexes, statement patterns]

    install --> i1{CUBRIDdb or pycubrid?}
    i1 -->|CUBRIDdb| i2[Build CUBRIDdb from cubrid-python v11.3.0.51+ or switch to cubrid+pycubrid]
    i1 -->|pycubrid| i3[Install pycubrid and use cubrid+pycubrid URL]

    conn --> c1{Broker reachable?}
    c1 -->|No| c2[Start broker / docker service and verify port 33000]
    c1 -->|Yes| c3[Enable pool_pre_ping and review SESSION_TIMEOUT]

    sql --> s1{Using unsupported feature?}
    s1 -->|RETURNING / ARRAY| s2[Refactor using supported CUBRID patterns]
    s1 -->|No| s3[Enable SQLAlchemy echo and inspect raw SQL]

    mig --> m1[Split migration into atomic DDL steps]
    perf --> p1[Profile query plan, batch size, lock scope]

재현 가능성으로 시작

실패 사례를 엔진 하나, 테이블 하나, 쿼리 하나인 최소 스크립트로 축소하세요. 문제가 드라이버인지, 방언인지, 애플리케이션 로직인지 빠르게 식별할 수 있습니다.

컴파일 오류와 런타임 오류 구분

  • 컴파일 오류는 SQL이 전송되기 전에 발생.
  • 런타임 오류는 CUBRID나 드라이버에서 발생.
  • 디버깅 경로가 다릅니다.

이슈 리포트에 환경 상세 포함

방언 버전, SQLAlchemy 버전, 드라이버(CUBRIDdb 또는 pycubrid), CUBRID 서버 버전, 사용한 정확한 연결 URL 형식을 포함하세요.

SQL 로깅 활성화

# 방법 1: echo=True
engine = create_engine("cubrid://dba@localhost:33000/testdb", echo=True)

# 방법 2: Python 로깅
import logging
logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG)

출력에는 SQL 문, 파라미터, 실행 시간이 포함됩니다.

방언 버전 확인

import sqlalchemy_cubrid
print(sqlalchemy_cubrid.__version__)  # 예: "1.8.0"

from sqlalchemy import create_engine
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
print(engine.dialect.name)            # "cubrid"
print(engine.dialect.server_version_info)  # (11, 2, 0, 378)

컴파일된 SQL 검사

from sqlalchemy import column, select, table

# 실행 없이 CUBRID용 문장 컴파일
from sqlalchemy_cubrid.dialect import CubridDialect

users = table("users", column("id"), column("name"))
stmt = select(users).where(users.c.name == "Alice")
compiled = stmt.compile(dialect=CubridDialect())
print(str(compiled))
print(compiled.params)

연결 테스트 스크립트

#!/usr/bin/env python3
"""Quick sqlalchemy-cubrid connection test."""
import sys
from sqlalchemy import create_engine, text, inspect

url = "cubrid+pycubrid://dba@localhost:33000/testdb"
try:
    engine = create_engine(url)
    with engine.connect() as conn:
        version = conn.execute(text("SELECT VERSION()")).scalar()
        print(f"✅ Connected to CUBRID {version}")

        inspector = inspect(engine)
        tables = inspector.get_table_names()
        print(f"✅ Found {len(tables)} tables: {tables[:5]}{'...' if len(tables) > 5 else ''}")

    print("✅ All checks passed")
except Exception as e:
    print(f"❌ Failed: {e}")
    sys.exit(1)

참고: 연결 가이드 · 타입 매핑 · DML 확장 · Alembic · 기능 지원