Type System¶
Complete reference for pycubrid's PEP 249 type objects, constructors, and CUBRID CCI data type codes.
Table of Contents¶
- Overview
- Declared SQL Types vs Fetch/Description Types
- DBAPIType Class
- PEP 249 Type Objects
- STRING
- BINARY
- NUMBER
- DATETIME
- ROWID
- PEP 249 Constructors
- CUBRID CCI_U_TYPE Codes
- Fetch Conversion Table
- LOB Type Handling
- Collection Types
- JSON Columns
decode_collections- Binding Collections
- Usage Examples
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, orTIMESTAMPTZ. - Fetch/description types are the
CUBRIDDataTypeinteger codes exposed throughcursor.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.
| 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.
| Member Type | CCI Code | CUBRID SQL Type |
|---|---|---|
| BIT | 5 | BIT(n) |
| VARBIT | 6 | BIT VARYING(n) |
| BLOB | 23 | BLOB |
NUMBER¶
Describes numeric columns.
| 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.
| 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.
| 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:
TIMESTAMPLTZandDATETIMELTZvalues are returned as aware datetimes in the session time zone, not in the zone that was active when the value was stored. Set it withSET TIME ZONE '<zone>'. Since pycubrid 1.8.0 the zone persists acrosscommit()/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-applySET TIME ZONEas 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.TIMESTAMPTZandDATETIMETZcarry their own zone.Repeated DST hour (#819): a bound aware
datetimewith a regiontzinfo(ZoneInfo) in a repeated wall time keeps itsfold. Afold=0value keeps the region-only literal, which CUBRID resolves to the first occurrence. Forfold=1pycubrid 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 zeroTIMESTAMP,TIMESTAMPTZ,TIMESTAMPLTZ,DATETIMETZandDATETIMELTZvalues, but Python'sdatetimehas no year 0. Fetching one raisesDataErrornaming the CUBRID type and fields, onexecute()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 sameDataErroruntil the nextexecute()(#507). Any other temporal field Python cannot hold is reported the same way. There is no option to returnNoneor text instead: convert the value in SQL, for exampleNULLIF(d, DATE'0000-00-00')(zero becomesNULL),CASE WHEN d = DATE'0000-00-00' THEN NULL ELSE d END, orTO_CHAR(d, 'YYYY-MM-DD')(returns'0000-00-00'). The explicit prepared API (pycubrid.compat.native) stays fail-closed: it raisesOperationalErrorand 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 UTCfor the LTZ types). pycubrid resolves:
- an offset to a fixed
datetime.timezone;- a region to
zoneinfo.ZoneInfo(region). The abbreviation picksfoldwhen a wall time occurs twice as daylight saving time ends, soAmerica/New_York ESTat 01:30 on 2026-11-01 is UTC-05:00 (fold=1) andEDTis UTC-04:00. A missing or unknown abbreviation, or one both occurrences share (Europe/Moscow MSKon 2014-10-26), keepsfold=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
DataErrornaming 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 raisesOperationalErrorand retires the session.zoneinforeads the system database or thetzdatapackage; on Windows pycubrid installstzdataautomatically, and on minimal Linux images installtzdata(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
Lobobjects as query parameters toexecute()or the defaultexecutemany(). Insert strings directly into CLOB columns and bytes directly into BLOB columns. The exception isexecutemany(..., prepared=True)on the sync cursor with autocommit off, which binds aLobof 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.descriptionreports the collection code (16, 17 or 18), not its element type. Integer elements remain Pythonint; 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:Truefor nullable columns andFalsefor NOT NULL/primary-key columns, not the inverse CASis_non_nullflag. - A nonempty collection whose elements are all SQL NULL (for example
{NULL}or{NULL, NULL}) decodes to[None, ...]forMULTISET/SEQUENCEandfrozenset({None})forSET. An empty collection decodes to[]/frozenset(). - An empty
BIT/BIT VARYINGelement (X'', wire length 0) decodes tob""and a NULL element (length -1) toNone, soSEQUENCE{X'', NULL, X'01'}is[b"", None, b"\x01"](#998). A zero-length element of any other type isNone. - Nested collection payloads remain raw
bytes. - Unknown collection element types fall back to raw
bytes. SETvalues are normalized to afrozensetwhen every decoded element is hashable. If a set contains unhashable elements (for example decoded JSON objects), pycubrid returns atupleto 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