Skip to content

Type System

Complete reference for pycubrid's PEP 249 type objects, constructors, and CUBRID CCI data type codes.


Table of Contents


Declared SQL Types vs Fetch/Description Types

This guide separates two different views of type information:

  • Declared SQL types are the column types you define in DDL and bind in SQL, such as VARCHAR, JSON, SET, BLOB, or TIMESTAMPTZ.
  • Fetch/description types are the CUBRIDDataType integer codes exposed through cursor.description[n][1] and the Python objects pycubrid returns after decoding wire data.

The PEP 249 type-object sections below group declared SQL types into the standard DB-API families. Later sections list the raw CUBRIDDataType codes and the exact Python values returned on fetch.


Overview

pycubrid implements the DB-API 2.0 (PEP 249) type system with:

  • 5 type objects — STRING, BINARY, NUMBER, DATETIME, ROWID
  • 7 constructors — Date, Time, Timestamp, DateFromTicks, TimeFromTicks, TimestampFromTicks, Binary
  • 27+ CCI data type codes — mapping CUBRID wire types to Python types, including JSON and collection support

Type objects enable comparison with cursor.description type codes:

import pycubrid

conn = pycubrid.connect(host="localhost", port=33000, database="testdb")
cur = conn.cursor()
cur.execute("SELECT name, age, created_at FROM users")

for col in cur.description:
    col_name = col[0]
    col_type = col[1]

    if col_type == pycubrid.STRING:
        print(f"{col_name} is a string column")
    elif col_type == pycubrid.NUMBER:
        print(f"{col_name} is a numeric column")
    elif col_type == pycubrid.DATETIME:
        print(f"{col_name} is a date/time column")

DBAPIType Class

The DBAPIType class implements the PEP 249 type comparison protocol. Each instance wraps a frozenset of integer type codes and compares equal to any code in that set.

from pycubrid.types import DBAPIType

# Create a custom type object
MY_TYPE = DBAPIType("MY_TYPE", frozenset({1, 2, 3}))

# Comparison with integer type codes
assert MY_TYPE == 1      # True — 1 is in the set
assert MY_TYPE == 4      # False — 4 is not in the set
assert MY_TYPE != 4      # True

# Comparison with other DBAPIType instances
OTHER = DBAPIType("OTHER", frozenset({1, 2, 3}))
assert MY_TYPE == OTHER  # True — same value sets

Methods

Method Description
__eq__(other) Returns True if other (int) is in values, or if other (DBAPIType) has the same values set
__ne__(other) Negation of __eq__
__hash__() Hash based on the frozen set of values
__repr__() Returns DBAPIType('name')

PEP 249 Type Objects

STRING

Describes string-based columns.

from pycubrid import STRING
Member Type CCI Code CUBRID SQL Type
CHAR 1 CHAR(n)
STRING (VARCHAR) 2 VARCHAR(n)
NCHAR 3 NCHAR(n)
VARNCHAR 4 NCHAR VARYING(n)
ENUM 25 ENUM
CLOB 24 CLOB
JSON 34 JSON

BINARY

Describes binary data columns.

from pycubrid import BINARY
Member Type CCI Code CUBRID SQL Type
BIT 5 BIT(n)
VARBIT 6 BIT VARYING(n)
BLOB 23 BLOB

NUMBER

Describes numeric columns.

from pycubrid import NUMBER
Member Type CCI Code CUBRID SQL Type Python Type
SHORT 9 SMALLINT int
INT 8 INTEGER int
BIGINT 21 BIGINT int
FLOAT 11 FLOAT float
DOUBLE 12 DOUBLE float
NUMERIC 7 NUMERIC(p, s) Decimal
MONETARY 10 MONETARY float

DATETIME

Describes date and time columns.

from pycubrid import DATETIME
Member Type CCI Code CUBRID SQL Type Python Type
DATE 13 DATE datetime.date
TIME 14 TIME datetime.time
TIMESTAMP 15 TIMESTAMP datetime.datetime
DATETIME 22 DATETIME datetime.datetime
TIMESTAMPTZ 29 TIMESTAMPTZ datetime.datetime
TIMESTAMPLTZ 30 TIMESTAMPLTZ datetime.datetime
DATETIMETZ 31 DATETIMETZ datetime.datetime
DATETIMELTZ 32 DATETIMELTZ datetime.datetime

ROWID

Describes row identifier columns.

from pycubrid import ROWID
Member Type CCI Code CUBRID SQL Type Python Type
OBJECT 19 OBJECT (OID) str ("OID:@page|slot|volume")

PEP 249 Constructors

These functions create Python objects suitable for use as query parameters.

Constructor Signature Returns Description
Date (year, month, day) datetime.date Calendar date
Time (hour, minute, second) datetime.time Time of day
Timestamp (year, month, day, hour, minute, second) datetime.datetime Date and time
DateFromTicks (ticks) datetime.date Date from Unix timestamp
TimeFromTicks (ticks) datetime.time Time from Unix timestamp
TimestampFromTicks (ticks) datetime.datetime Datetime from Unix timestamp
Binary (value) bytes Binary data from bytes, bytearray, or str (UTF-8)

Usage

import pycubrid

# Date/Time constructors
d = pycubrid.Date(2025, 3, 15)
t = pycubrid.Time(14, 30, 0)
ts = pycubrid.Timestamp(2025, 3, 15, 14, 30, 0)

# From Unix timestamps
d2 = pycubrid.DateFromTicks(1710500000.0)
t2 = pycubrid.TimeFromTicks(1710500000.0)
ts2 = pycubrid.TimestampFromTicks(1710500000.0)

# Binary constructor
b1 = pycubrid.Binary(b"\x00\x01\x02")           # from bytes
b2 = pycubrid.Binary(bytearray([0, 1, 2]))      # from bytearray
b3 = pycubrid.Binary("hello")                    # from str → UTF-8 encoded bytes

CUBRID CCI_U_TYPE Codes

These integer codes are used on the CAS wire protocol and appear in cursor.description[n][1].

CUBRIDDataType.NULL is an alias of CUBRIDDataType.UNKNOWN (both are 0): CUBRIDDataType.NULL is CUBRIDDataType.UNKNOWN, and CUBRIDDataType(0).name is "UNKNOWN".

Constant Code CUBRID Type Category
UNKNOWN / NULL 0 NULL —
CHAR 1 CHAR(n) STRING
STRING 2 VARCHAR(n) STRING
NCHAR 3 NCHAR(n) STRING
VARNCHAR 4 NCHAR VARYING(n) STRING
BIT 5 BIT(n) BINARY
VARBIT 6 BIT VARYING(n) BINARY
NUMERIC 7 NUMERIC(p, s) NUMBER
INT 8 INTEGER NUMBER
SHORT 9 SMALLINT NUMBER
MONETARY 10 MONETARY NUMBER
FLOAT 11 FLOAT NUMBER
DOUBLE 12 DOUBLE NUMBER
DATE 13 DATE DATETIME
TIME 14 TIME DATETIME
TIMESTAMP 15 TIMESTAMP DATETIME
SET 16 SET Collection
MULTISET 17 MULTISET Collection
SEQUENCE 18 SEQUENCE / LIST Collection
OBJECT 19 OBJECT (OID) ROWID
RESULTSET 20 Result set —
BIGINT 21 BIGINT NUMBER
DATETIME 22 DATETIME DATETIME
BLOB 23 BLOB BINARY
CLOB 24 CLOB STRING
ENUM 25 ENUM STRING
JSON 34 JSON STRING
TIMESTAMPTZ 29 TIMESTAMPTZ DATETIME
TIMESTAMPLTZ 30 TIMESTAMPLTZ DATETIME
DATETIMETZ 31 DATETIMETZ DATETIME
DATETIMELTZ 32 DATETIMELTZ DATETIME

These codes are available as the CUBRIDDataType enum:

from pycubrid.constants import CUBRIDDataType

print(CUBRIDDataType.INT)       # CUBRIDDataType.INT (8)
print(CUBRIDDataType.VARCHAR)   # AttributeError — use CUBRIDDataType.STRING (2)

Fetch Conversion Table

How pycubrid converts CUBRID wire types to Python objects when fetching results:

CUBRID Type CCI Code Python Type Notes
CHAR, VARCHAR, NCHAR, NCHAR VARYING, ENUM 1–4, 25 str Null-terminated, decoded with the connection charset (default UTF-8); undecodable bytes raise DataError naming the codec (connection stays usable). In an EUC-KR database CHAR(n) pads with U+3000
SHORT (SMALLINT) 9 int 16-bit signed
INTEGER 8 int 32-bit signed
BIGINT 21 int 64-bit signed
FLOAT 11 float IEEE 754 single
DOUBLE, MONETARY 12, 10 float IEEE 754 double
NUMERIC / DECIMAL 7 Decimal Exact numeric (string-parsed)
DATE 13 datetime.date Calendar date
TIME 14 datetime.time Time of day (second precision; a bound datetime.time drops its microseconds and tzinfo without an error, see parameter binding)
TIMESTAMP 15 datetime.datetime Date + time (microsecond = 0)
DATETIME 22 datetime.datetime Date + time + millisecond
TIMESTAMPTZ, TIMESTAMPLTZ 29, 30 datetime.datetime Timezone-aware timestamps (second precision, microsecond = 0)
DATETIMETZ, DATETIMELTZ 31, 32 datetime.datetime Timezone-aware datetimes (millisecond precision)
BIT, BIT VARYING 5, 6 bytes Raw binary data; an empty BIT/BIT VARYING value (X'', B'') is b"", distinct from NULL (#994). This follows the JDBC driver; the official CUBRIDdb driver returns None for X''
JSON 34 str or Any Raw JSON string by default; decoded when json_deserializer= is set. Always UTF-8, whatever the connection charset
SET, MULTISET, SEQUENCE 16, 17, 18 bytes or decoded collection Decoded only when decode_collections=True
OBJECT (OID) 19 str Format: "OID:@page\|slot\|volume"
BLOB 23 dict LOB handle (see below)
CLOB 24 dict LOB handle (see below); Lob.read() returns bytes in the column charset, not decoded with charset
NULL / UNKNOWN 0 None —

Local time zone types: TIMESTAMPLTZ and DATETIMELTZ values are returned as aware datetimes in the session time zone, not in the zone that was active when the value was stored. Set it with SET TIME ZONE '<zone>'. Since pycubrid 1.8.0 the zone persists across commit()/rollback() while the CAS session is kept. If the CAS closes the socket after a transaction boundary (CAS restart, CHANGE CLIENT, cubrid broker reset), the driver reconnects and the new session uses the server default zone, so re-apply SET TIME ZONE as with other SQL session state (see CAS recycled at a transaction boundary). If the session zone changes, the same stored instant is returned with a different UTC offset. TIMESTAMPTZ and DATETIMETZ carry their own zone.

Repeated DST hour (#819): a bound aware datetime with a region tzinfo (ZoneInfo) in a repeated wall time keeps its fold. A fold=0 value keeps the region-only literal, which CUBRID resolves to the first occurrence. For fold=1 pycubrid adds the zone abbreviation (America/New_York EST), or sends the offset alone when the abbreviation is numeric or shared by both occurrences, so the second occurrence is no longer stored as the first. A skipped spring-forward wall time is rejected by CUBRID. See parameter binding.

Zero dates (#512): CUBRID accepts zero values such as DATE'0000-00-00', DATETIME'0000-00-00 00:00:00' and zero TIMESTAMP, TIMESTAMPTZ, TIMESTAMPLTZ, DATETIMETZ and DATETIMELTZ values, but Python's datetime has no year 0. Fetching one raises DataError naming the CUBRID type and fields, on execute() and on a later fetch page alike; the connection stays usable, and the cursor keeps its server handle as for invalid UTF-8 (#492). On a later page, rows fetched before that page are still returned, and fetches after them keep raising the same DataError until the next execute() (#507). Any other temporal field Python cannot hold is reported the same way. There is no option to return None or text instead: convert the value in SQL, for example NULLIF(d, DATE'0000-00-00') (zero becomes NULL), CASE WHEN d = DATE'0000-00-00' THEN NULL ELSE d END, or TO_CHAR(d, 'YYYY-MM-DD') (returns '0000-00-00'). The explicit prepared API (pycubrid.compat.native) stays fail-closed: it raises OperationalError and retires the session. See Zero Date or Datetime Value.

Zone decoding (#413): CUBRID sends each value's zone as text: an offset (+05:30) or a region name with the abbreviation in effect (Asia/Seoul KST, UTC UTC for the LTZ types). pycubrid resolves:

  • an offset to a fixed datetime.timezone;
  • a region to zoneinfo.ZoneInfo(region). The abbreviation picks fold when a wall time occurs twice as daylight saving time ends, so America/New_York EST at 01:30 on 2026-11-01 is UTC-05:00 (fold=1) and EDT is UTC-04:00. A missing or unknown abbreviation, or one both occurrences share (Europe/Moscow MSK on 2014-10-26), keeps fold=0;
  • an empty zone to a naive datetime.

A region the client's time zone database does not know, or an offset that is malformed or not strictly within ±24 hours, raises DataError naming the zone, rather than silently returning a naive value. The connection stays usable. The explicit prepared API (pycubrid.compat.native) stays fail-closed as for invalid UTF-8 (#492): it raises OperationalError and retires the session. zoneinfo reads the system database or the tzdata package; on Windows pycubrid installs tzdata automatically, and on minimal Linux images install tzdata (pip or the OS package). See Unresolved Time Zone in a TZ Value.


LOB Type Handling

LOB columns (BLOB and CLOB) are not returned as content. Instead, they return a handle dictionary that can be used with the Lob class to read the actual data.

LOB Handle Structure

When you fetch a BLOB or CLOB column, you receive:

{
    "lob_type": 23,                    # CUBRIDDataType.BLOB (23) or CLOB (24)
    "lob_length": 1024,                # Size of the LOB content in bytes
    "file_locator": "file://.../...",   # Server-side file locator
    "packed_lob_handle": b"...",        # Raw handle bytes for Lob.read()
}

Reading LOB Content

from pycubrid.lob import Lob
from pycubrid.constants import CUBRIDDataType

conn = pycubrid.connect(host="localhost", port=33000, database="testdb")
cur = conn.cursor()

# Insert string data directly into CLOB column
cur.execute("INSERT INTO cookbook_docs (content) VALUES ('Hello, CLOB!')")
conn.commit()

# Fetch returns a LOB handle dict, not the content
cur.execute("SELECT content FROM cookbook_docs WHERE id = 1")
row = cur.fetchone()
lob_handle = row[0]  # dict with lob_type, lob_length, etc.

# To read content, use Lob class with the packed handle
lob = Lob(conn, CUBRIDDataType.CLOB, lob_handle["packed_lob_handle"])
content = lob.read(lob_handle["lob_length"])
print(content)  # b'Hello, CLOB!'

Important: You cannot pass Lob objects as query parameters to execute() or the default executemany(). Insert strings directly into CLOB columns and bytes directly into BLOB columns. The exception is executemany(..., prepared=True) on the sync cursor with autocommit off, which binds a Lob of the same connection by its handle (API_REFERENCE.md).


Collection Types

CUBRID's collection types (SET, MULTISET, SEQUENCE) default to raw bytes for backward compatibility. When decode_collections=True is passed to connect() or pycubrid.aio.connect(), pycubrid decodes supported collection payloads into Python containers.

CUBRID Type CCI Code decode_collections=False decode_collections=True
SET 16 bytes frozenset when hashable, otherwise tuple
MULTISET 17 bytes list
SEQUENCE 18 bytes list

Notes:

  • cursor.description reports the collection code (16, 17 or 18), not its element type. Integer elements remain Python int; MULTISET preserves duplicates and SEQUENCE preserves order. The same decoding rules apply to sync and async fetches.
  • The final description field, null_ok, uses the PEP 249 meaning: True for nullable columns and False for NOT NULL/primary-key columns, not the inverse CAS is_non_null flag.
  • A nonempty collection whose elements are all SQL NULL (for example {NULL} or {NULL, NULL}) decodes to [None, ...] for MULTISET/SEQUENCE and frozenset({None}) for SET. An empty collection decodes to [] / frozenset().
  • An empty BIT/BIT VARYING element (X'', wire length 0) decodes to b"" and a NULL element (length -1) to None, so SEQUENCE{X'', NULL, X'01'} is [b"", None, b"\x01"] (#998). A zero-length element of any other type is None.
  • Nested collection payloads remain raw bytes.
  • Unknown collection element types fall back to raw bytes.
  • SET values are normalized to a frozenset when every decoded element is hashable. If a set contains unhashable elements (for example decoded JSON objects), pycubrid returns a tuple to preserve the contents without raising.

JSON Columns

JSON columns use CUBRID type code 34.

Connection option Fetch result
default (json_deserializer=None) raw JSON str
json_deserializer=json.loads decoded Python object
json_deserializer=custom_callable return value of the callable

This opt-in behavior keeps fetches allocation-light when applications prefer to defer JSON parsing.

decode_collections

decode_collections is a connection-level switch on both pycubrid.connect() and pycubrid.aio.connect().

Setting Result
False (default) Return collection payloads as raw CAS wire bytes
True Decode supported SET, MULTISET, and SEQUENCE payloads into Python containers

Binding Collections

Plain Python set, frozenset, list and tuple values are rejected as parameters. To bind a collection, wrap its elements in one of the typed collection parameters (#567):

Class Rendered literal Server semantics
pycubrid.types.Set SET{...} duplicates removed, order not kept
pycubrid.types.Multiset MULTISET{...} duplicates kept, order not kept
pycubrid.types.Sequence SEQUENCE{...} (same type as LIST{...}) duplicates and order kept
from pycubrid.types import Multiset, Sequence, Set

cur.execute("INSERT INTO t VALUES (?, ?, ?)", (Set([1, 2]), Multiset(["a", "a"]), Sequence([3, 1])))

The classes are also exported from the top-level pycubrid package. Each one is immutable, stores its elements as a tuple (.elements) and cannot be subclassed. Elements accept the same types as scalar parameters (None, bool, int, float, Decimal, str, bytes, bytearray, date, time, datetime) and are rendered by the same hardened renderer; nested collections raise ProgrammingError. Both sync and async ordinary cursors support them. See Parameter Binding.

A dict is rejected (TypeError) by all three classes — iterating it would silently use only its keys and drop the values. Sequence also rejects a set/frozenset (TypeError), since their iteration order is not guaranteed and would make an ordered collection's element order nondeterministic; Set and Multiset accept a set/frozenset since their own server-side semantics already discard order. The instances are safe to copy.copy() (always returns the same object), copy.deepcopy() (the same object when every element is itself immutable; an independent copy, with independently copied elements, when an element such as bytearray is mutable, so mutating the copy cannot alias back into the original) and pickle (round-trips to an equal instance), and re-invoking __init__ on an existing instance cannot mutate it.

Set, Multiset and Sequence are ordinary names in pycubrid.types and pycubrid, not typing aliases, but Sequence in particular is also a name from the standard typing/collections.abc modules. from pycubrid import * brings pycubrid's Set and Sequence into scope and shadows any typing.Set/typing.Sequence (or collections.abc.Sequence) imported the same way; prefer an explicit import such as from pycubrid.types import Sequence as CubridSequence (or import pycubrid and use pycubrid.Sequence) when both are needed in the same module.

Decoding is unchanged: fetched collections are plain Python containers as in the table above (with decode_collections=True), never these parameter types.


Usage Examples

Type Checking with cursor.description

import pycubrid

conn = pycubrid.connect(host="localhost", port=33000, database="testdb")
cur = conn.cursor()
cur.execute("SELECT * FROM users LIMIT 1")

for col in cur.description:
    name, type_code, _, _, precision, scale, nullable = col
    if type_code == pycubrid.STRING:
        print(f"  {name}: STRING (precision={precision})")
    elif type_code == pycubrid.NUMBER:
        print(f"  {name}: NUMBER (precision={precision}, scale={scale})")
    elif type_code == pycubrid.DATETIME:
        print(f"  {name}: DATETIME")
    elif type_code == pycubrid.BINARY:
        print(f"  {name}: BINARY")
    elif type_code == pycubrid.ROWID:
        print(f"  {name}: ROWID (OID)")
    else:
        print(f"  {name}: type_code={type_code}")

cur.close()
conn.close()

Parameter Binding with Constructors

import pycubrid

conn = pycubrid.connect(host="localhost", port=33000, database="testdb")
cur = conn.cursor()

# Use PEP 249 constructors for parameter values
cur.execute(
    "INSERT INTO cookbook_events (event_date, event_time, created_at) VALUES (?, ?, ?)",
    [
        pycubrid.Date(2025, 12, 25),
        pycubrid.Time(10, 0, 0),
        pycubrid.Timestamp(2025, 3, 15, 14, 30, 0),
    ],
)
conn.commit()
cur.close()
conn.close()

Decimal Parameters

Decimal parameters are sent as plain fixed-point literals (never E notation), so CUBRID keeps them NUMERIC with the scale you wrote: Decimal("0.0000001") fetches back as Decimal, not float. A value whose plain literal needs more than 38 digits, CUBRID's maximum NUMERIC precision, raises DataError instead of becoming DOUBLE. See Parameter Binding: Decimal parameters.

from decimal import Decimal

cur.execute("SELECT ?", [Decimal("0.0000001")])  # sent as 0.0000001
assert cur.fetchone()[0] == Decimal("0.0000001")
assert cur.description[0][1] == pycubrid.constants.CUBRIDDataType.NUMERIC

Using CUBRIDDataType Enum

from pycubrid.constants import CUBRIDDataType

# Check specific type codes
assert CUBRIDDataType.INT == 8
assert CUBRIDDataType.STRING == 2
assert CUBRIDDataType.DATETIME == 22

# Use in type switch logic
def describe_type(code: int) -> str:
    match code:
        case CUBRIDDataType.INT | CUBRIDDataType.BIGINT | CUBRIDDataType.SHORT:
            return "integer"
        case CUBRIDDataType.FLOAT | CUBRIDDataType.DOUBLE:
            return "floating point"
        case CUBRIDDataType.NUMERIC:
            return "exact decimal"
        case CUBRIDDataType.STRING | CUBRIDDataType.CHAR:
            return "text"
        case _:
            return f"other ({code})"

See also: API Reference · Examples · Connection Guide