C API 벡터
C API 벡터 (Vectors)
Vector는 열의 수평 슬라이스를 나타내요. 배열과 비슷하게 특정 타입의 값을 여럿 담아요. Vector는 DuckDB에서 사용되는 핵심 데이터 표현이에요. Vector는 보통 데이터 청크 안에 저장돼요.
vector와 data chunk 인터페이스는 DuckDB와 상호작용하는 가장 효율적인 방법으로, 최고의 성능을 제공해요. 다만 사용하기 어렵기도 하고, 사용할 때 주의가 필요해요.
출처: 문서
본문
Vector 형식 (Vector Format)
Vector는 특정 데이터 타입의 배열이에요. vector의 논리적 타입은 duckdb_vector_get_column_type으로 얻을 수 있고, 논리적 타입의 타입 id는 duckdb_get_type_id로 얻을 수 있어요.
Vector 자체에는 크기가 없어요. 대신 부모 데이터 청크에 크기가 있어요(duckdb_data_chunk_get_size로 얻음). 어떤 데이터 청크에 속한 모든 vector는 같은 크기를 가져요.
기본 타입 (Primitive Types)
기본 타입의 경우 duckdb_vector_get_data 메서드로 기저 배열을 얻을 수 있어요. 배열은 올바른 네이티브 타입으로 접근하면 돼요. 아래 표는 duckdb_type을 배열의 네이티브 타입으로 매핑한 것이에요.
| duckdb_type | NativeType |
|---|---|
| DUCKDB_TYPE_BOOLEAN | bool |
| DUCKDB_TYPE_TINYINT | int8_t |
| DUCKDB_TYPE_SMALLINT | int16_t |
| DUCKDB_TYPE_INTEGER | int32_t |
| DUCKDB_TYPE_BIGINT | int64_t |
| DUCKDB_TYPE_UTINYINT | uint8_t |
| DUCKDB_TYPE_USMALLINT | uint16_t |
| DUCKDB_TYPE_UINTEGER | uint32_t |
| DUCKDB_TYPE_UBIGINT | uint64_t |
| DUCKDB_TYPE_FLOAT | float |
| DUCKDB_TYPE_DOUBLE | double |
| DUCKDB_TYPE_TIMESTAMP | duckdb_timestamp |
| DUCKDB_TYPE_DATE | duckdb_date |
| DUCKDB_TYPE_TIME | duckdb_time |
| DUCKDB_TYPE_INTERVAL | duckdb_interval |
| DUCKDB_TYPE_HUGEINT | duckdb_hugeint |
| DUCKDB_TYPE_UHUGEINT | duckdb_uhugeint |
| DUCKDB_TYPE_VARCHAR | duckdb_string_t |
| DUCKDB_TYPE_BLOB | duckdb_string_t |
| DUCKDB_TYPE_TIMESTAMP_S | duckdb_timestamp |
| DUCKDB_TYPE_TIMESTAMP_MS | duckdb_timestamp |
| DUCKDB_TYPE_TIMESTAMP_NS | duckdb_timestamp |
| DUCKDB_TYPE_UUID | duckdb_hugeint |
| DUCKDB_TYPE_TIME_TZ | duckdb_time_tz |
| DUCKDB_TYPE_TIMESTAMP_TZ | duckdb_timestamp |
NULL 값 (NULL Values)
vector의 어떤 값이든 NULL일 수 있어요. 값이 NULL이면 해당 인덱스의 기본 배열에 있는 값은 정의되지 않아요(초기화되지 않았을 수 있어요). 유효성 마스크(validity mask)는 uint64_t 요소로 구성된 비트마스크예요. vector의 64 값마다 uint64_t 요소가 하나 존재해요(올림). 유효성 마스크는 값이 유효하면 비트가 1, 유효하지 않으면(즉 NULL) 0으로 설정돼요.
비트마스크의 비트는 직접 읽거나, 값이 NULL인지 확인하는 더 느린 헬퍼 메서드 duckdb_validity_row_is_valid를 사용할 수 있어요.
duckdb_vector_get_validity는 유효성 마스크에 대한 포인터를 반환해요. vector의 모든 값이 유효하면 이 함수가 nullptr을 반환하기 도 하므로, 그 경우에는 유효성 마스크를 확인할 필요가 없어요.
문자열 (Strings)
문자열 값은 duckdb_string_t로 저장돼요. 이는 문자열을 인라인으로 저장하거나(짧은 경우, 즉 <= 12 bytes), 12바이트보다 길면 문자열 데이터에 대한 포인터를 저장하는 특별한 struct예요.
typedef struct {
union {
struct {
uint32_t length;
char prefix[4];
char *ptr;
} pointer;
struct {
uint32_t length;
char inlined[12];
} inlined;
} value;
} duckdb_string_t;
length는 직접 접근할 수 있고, 문자열이 인라인인지 확인하려면 duckdb_string_is_inlined를 사용할 수 있어요.
Decimal (Decimals)
Decimal은 내부적으로 정수 값으로 저장돼요. 정확한 네이티브 타입은 decimal 타입의 width에 따라 달라지며, 다음 표와 같아요:
| Width | NativeType |
|---|---|
| <= 4 | int16_t |
| <= 9 | int32_t |
| <= 18 | int64_t |
| <= 38 | duckdb_hugeint |
decimal의 내부 타입은 duckdb_decimal_internal_type으로 얻을 수 있어요.
Decimal은 10^scale을 곱한 정수 값으로 저장돼요. decimal의 scale은 duckdb_decimal_scale로 얻을 수 있어요. 예를 들어 타입이 DECIMAL(8, 3)인 decimal 값 10.5는 내부적으로 10500이라는 int32_t 값으로 저장돼요. 올바른 decimal 값을 얻으려면 그 값을 적절한 10의 거듭제곱으로 나눠야 해요.
열거형 (Enums)
Enum은 내부적으로 부호 없는 정수 값으로 저장돼요. 정확한 네이티브 타입은 enum 사전의 크기에 따라 달라지며, 다음 표와 같아요:
| Dictionary size | NativeType |
|---|---|
| <= 255 | uint8_t |
| <= 65535 | uint16_t |
| <= 4294967295 | uint32_t |
enum의 내부 타입은 duckdb_enum_internal_type으로 얻을 수 있어요.
enum의 실제 문자열 값을 얻으려면 duckdb_enum_dictionary_value 함수로 주어진 사전 항목에 해당하는 enum 값을 얻어야 해요. enum 사전은 열 전체에 대해 동일하므로 한 번만 구성하면 돼요.
구조체 (Structs)
Struct는 여러 개의 자식 타입을 담을 수 있는 중첩 타입이에요. C의 struct처럼 생각하면 돼요. vector를 사용해 struct 데이터에 접근하는 방법은 duckdb_struct_vector_get_child 메서드로 자식 vector를 재귀적으로 접근하는 것이에요.
struct vector 자체에는 데이터가 없어요(즉 struct에 duckdb_vector_get_data 메서드를 쓰면 안 돼요). 그러나 struct vector 자체에는 유효성 마스크가 있어요. 그 이유는 struct의 자식 요소가 NULL일 수 있을 뿐 아니라 struct 자체도 NULL일 수 있기 때문이에요.
리스트 (Lists)
List는 행마다 x번 반복되는 단일 자식 타입을 담는 중첩 타입이에요. C의 가변 길이 배열처럼 생각하면 돼요. vector를 사용해 list 데이터에 접근하는 방법은 duckdb_list_vector_get_child 메서드로 자식 vector에 접근하는 것이에요.
duckdb_vector_get_data를 사용해 duckdb_list_entry로 저장된 리스트의 오프셋과 길이를 얻어야 하고, 이를 자식 vector에 적용할 수 있어요.
typedef struct {
uint64_t offset;
uint64_t length;
} duckdb_list_entry;
list 항목 자체 그리고 리스트에 저장된 자식도 모두 NULL일 수 있다는 점에 유의해요. 이는 다시 유효성 마스크로 확인해야 해요.
배열 (Arrays)
Array는 행마다 정확히 array_size번 반복되는 단일 자식 타입을 담는 중첩 타입이에요. C의 고정 크기 배열처럼 생각하면 돼요. Array는 list와 정확히 동일하게 동작하는데, 유일한 차이는 각 항목의 길이와 오프셋이 고정된다는 점이에요. 고정 배열 크기는 duckdb_array_type_array_size로 얻을 수 있어요. n번째 항목의 데이터는 offset = n * array_size에 있고 항상 length = array_size예요.
list와 마찬가지로 array도 여전히 NULL일 수 있다는 점에 유의하세요. 유효성 마스크로 확인해야 해요.
예시 (Examples)
아래는 vector와 상호작용하는 몇 가지 완전한 end-to-end 예시예요.
예시: NULL 값이 있는 int64 Vector 읽기
duckdb_database db;
duckdb_connection con;
duckdb_open(nullptr, &db);
duckdb_connect(db, &con);
duckdb_result res;
duckdb_query(con, "SELECT CASE WHEN i%2=0 THEN NULL ELSE i END res_col FROM range(10) t(i)", &res);
// iterate until result is exhausted
while (true) {
duckdb_data_chunk result = duckdb_fetch_chunk(res);
if (!result) {
// result is exhausted
break;
}
// get the number of rows from the data chunk
idx_t row_count = duckdb_data_chunk_get_size(result);
// get the first column
duckdb_vector res_col = duckdb_data_chunk_get_vector(result, 0);
// get the native array and the validity mask of the vector
int64_t *vector_data = (int64_t *) duckdb_vector_get_data(res_col);
uint64_t *vector_validity = duckdb_vector_get_validity(res_col);
// iterate over the rows
for (idx_t row = 0; row < row_count; row++) {
if (duckdb_validity_row_is_valid(vector_validity, row)) {
printf("%lld\n", vector_data[row]);
} else {
printf("NULL\n");
}
}
duckdb_destroy_data_chunk(&result);
}
// clean-up
duckdb_destroy_result(&res);
duckdb_disconnect(&con);
duckdb_close(&db);
예시: 문자열 Vector 읽기
duckdb_database db;
duckdb_connection con;
duckdb_open(nullptr, &db);
duckdb_connect(db, &con);
duckdb_result res;
duckdb_query(con, "SELECT CASE WHEN i%2=0 THEN CONCAT('short_', i) ELSE CONCAT('longstringprefix', i) END FROM range(10) t(i)", &res);
// iterate until result is exhausted
while (true) {
duckdb_data_chunk result = duckdb_fetch_chunk(res);
if (!result) {
// result is exhausted
break;
}
// get the number of rows from the data chunk
idx_t row_count = duckdb_data_chunk_get_size(result);
// get the first column
duckdb_vector res_col = duckdb_data_chunk_get_vector(result, 0);
// get the native array and the validity mask of the vector
duckdb_string_t *vector_data = (duckdb_string_t *) duckdb_vector_get_data(res_col);
uint64_t *vector_validity = duckdb_vector_get_validity(res_col);
// iterate over the rows
for (idx_t row = 0; row < row_count; row++) {
if (duckdb_validity_row_is_valid(vector_validity, row)) {
duckdb_string_t str = vector_data[row];
if (duckdb_string_is_inlined(str)) {
// use inlined string
printf("%.*s\n", str.value.inlined.length, str.value.inlined.inlined);
} else {
// follow string pointer
printf("%.*s\n", str.value.pointer.length, str.value.pointer.ptr);
}
} else {
printf("NULL\n");
}
}
duckdb_destroy_data_chunk(&result);
}
// clean-up
duckdb_destroy_result(&res);
duckdb_disconnect(&con);
duckdb_close(&db);
예시: Struct Vector 읽기
duckdb_database db;
duckdb_connection con;
duckdb_open(nullptr, &db);
duckdb_connect(db, &con);
duckdb_result res;
duckdb_query(con, "SELECT CASE WHEN i%5=0 THEN NULL ELSE {'col1': i, 'col2': CASE WHEN i%2=0 THEN NULL ELSE 100 + i * 42 END} END FROM range(10) t(i)", &res);
// iterate until result is exhausted
while (true) {
duckdb_data_chunk result = duckdb_fetch_chunk(res);
if (!result) {
// result is exhausted
break;
}
// get the number of rows from the data chunk
idx_t row_count = duckdb_data_chunk_get_size(result);
// get the struct column
duckdb_vector struct_col = duckdb_data_chunk_get_vector(result, 0);
uint64_t *struct_validity = duckdb_vector_get_validity(struct_col);
// get the child columns of the struct
duckdb_vector col1_vector = duckdb_struct_vector_get_child(struct_col, 0);
int64_t *col1_data = (int64_t *) duckdb_vector_get_data(col1_vector);
uint64_t *col1_validity = duckdb_vector_get_validity(col1_vector);
duckdb_vector col2_vector = duckdb_struct_vector_get_child(struct_col, 1);
int64_t *col2_data = (int64_t *) duckdb_vector_get_data(col2_vector);
uint64_t *col2_validity = duckdb_vector_get_validity(col2_vector);
// iterate over the rows
for (idx_t row = 0; row < row_count; row++) {
if (!duckdb_validity_row_is_valid(struct_validity, row)) {
// entire struct is NULL
printf("NULL\n");
continue;
}
// read col1
printf("{'col1': ");
if (!duckdb_validity_row_is_valid(col1_validity, row)) {
// col1 is NULL
printf("NULL");
} else {
printf("%lld", col1_data[row]);
}
printf(", 'col2': ");
if (!duckdb_validity_row_is_valid(col2_validity, row)) {
// col2 is NULL
printf("NULL");
} else {
printf("%lld", col2_data[row]);
}
printf("}\n");
}
duckdb_destroy_data_chunk(&result);
}
// clean-up
duckdb_destroy_result(&res);
duckdb_disconnect(&con);
duckdb_close(&db);
예시: List Vector 읽기
duckdb_database db;
duckdb_connection con;
duckdb_open(nullptr, &db);
duckdb_connect(db, &con);
duckdb_result res;
duckdb_query(con, "SELECT CASE WHEN i % 5 = 0 THEN NULL WHEN i % 2 = 0 THEN [i, i + 1] ELSE [i * 42, NULL, i * 84] END FROM range(10) t(i)", &res);
// iterate until result is exhausted
while (true) {
duckdb_data_chunk result = duckdb_fetch_chunk(res);
if (!result) {
// result is exhausted
break;
}
// get the number of rows from the data chunk
idx_t row_count = duckdb_data_chunk_get_size(result);
// get the list column
duckdb_vector list_col = duckdb_data_chunk_get_vector(result, 0);
duckdb_list_entry *list_data = (duckdb_list_entry *) duckdb_vector_get_data(list_col);
uint64_t *list_validity = duckdb_vector_get_validity(list_col);
// get the child column of the list
duckdb_vector list_child = duckdb_list_vector_get_child(list_col);
int64_t *child_data = (int64_t *) duckdb_vector_get_data(list_child);
uint64_t *child_validity = duckdb_vector_get_validity(list_child);
// iterate over the rows
for (idx_t row = 0; row < row_count; row++) {
if (!duckdb_validity_row_is_valid(list_validity, row)) {
// entire list is NULL
printf("NULL\n");
continue;
}
// read the list offsets for this row
duckdb_list_entry list = list_data[row];
printf("[");
for (idx_t child_idx = list.offset; child_idx < list.offset + list.length; child_idx++) {
if (child_idx > list.offset) {
printf(", ");
}
if (!duckdb_validity_row_is_valid(child_validity, child_idx)) {
// col1 is NULL
printf("NULL");
} else {
printf("%lld", child_data[child_idx]);
}
}
printf("]\n");
}
duckdb_destroy_data_chunk(&result);
}
// clean-up
duckdb_destroy_result(&res);
duckdb_disconnect(&con);
duckdb_close(&db);
API 참조 개요 (API Reference Overview)
이 섹션은 스크립트(scripts/generate_c_api_docs.py)로 생성되며, 각 함수의 시그니처와 설명·파라미터·반환값을 담고 있어요. 주요 함수는 다음과 같아요:
- 핵심 vector 함수:
duckdb_create_vector,duckdb_destroy_vector,duckdb_vector_get_column_type,duckdb_vector_get_data,duckdb_vector_get_validity,duckdb_vector_ensure_validity_writable,duckdb_vector_assign_string_element,duckdb_vector_assign_string_element_len,duckdb_list_vector_get_child,duckdb_list_vector_get_size,duckdb_list_vector_set_size,duckdb_list_vector_reserve,duckdb_struct_vector_get_child,duckdb_array_vector_get_child,duckdb_slice_vector,duckdb_vector_copy_sel,duckdb_vector_reference_value,duckdb_vector_reference_vector - 유효성 마스크 함수 (Validity Mask Functions):
duckdb_validity_row_is_valid,duckdb_validity_set_row_validity,duckdb_validity_set_row_invalid,duckdb_validity_set_row_valid
대표적인 함수 설명 몇 개를 살펴볼게요.
duckdb_create_vector
Flat vector를 생성해요. duckdb_destroy_vector로 파괴해야 해요.
duckdb_vector duckdb_create_vector(duckdb_logical_type type, idx_t capacity);
파라미터:
type: vector의 논리적 타입.capacity: vector의 용량.
반환값: vector.
duckdb_vector_get_column_type
지정한 vector의 열 타입을 가져와요. 결과는 duckdb_destroy_logical_type으로 파괴해야 해요.
duckdb_logical_type duckdb_vector_get_column_type(duckdb_vector vector);
파라미터:
vector: 데이터를 가져올 vector.
반환값: vector의 타입.
duckdb_vector_get_data
vector의 데이터 포인터를 가져와요. 데이터 포인터는 vector에서 값을 읽거나 쓰는 데 사용할 수 있어요. 값을 읽고 쓰는 방법은 vector의 타입에 따라 달라요.
void *duckdb_vector_get_data(duckdb_vector vector);
파라미터:
vector: 데이터 포인터를 가져올 vector.
이 자동 생성 API 참조의 나머지 함수들도 같은 패턴을 따라요: 각 함수는 시그니처, 설명, 파라미터, 반환값을 담고 있어요. 전체 세부 사항은 공식 C API 참조에서 확인할 수 있어요.