C++ API
C++ API
DuckDB C++ API로 데이터베이스를 제어하는 방법을 정리해 드릴게요. 다만 시작 전에 한 가지 중요한 점을 알려드릴게요. 아래 경고처럼 DuckDB의 C++ API는 내부용이라 안정성이 보장되지 않고 예고 없이 바뀔 수 있어요. DuckDB 위에 애플리케이션을 만들 계획이라면 [C API]({% link docs/current/clients/c/overview.md %})를 사용하는 걸 권장해요.
Installation DuckDB C++ API를 사용하려면 플랫폼에 맞는 [
libduckdb아카이브]({% link install/index.html %}?environment=c)를 다운로드하세요.최신 안정 버전의 DuckDB C++ API는 {{ site.current_duckdb_version }}이에요.
Warning DuckDB의 C++ API는 내부용이에요. 안정성이 보장되지 않고 예고 없이 바뀔 수 있어요. DuckDB 위에 애플리케이션을 만들려면 [C API]({% link docs/current/clients/c/overview.md %})를 사용하는 것을 권장해요.
출처: 문서
본문
설치
DuckDB C++ API는 libduckdb 패키지의 일부로 설치할 수 있어요. 자세한 내용은 [설치 페이지]({% link install/index.html %}?environment=c)를 참고하세요.
기본 API 사용법
DuckDB는 사용자 정의 C++ API를 구현해요. 이 API는 데이터베이스 인스턴스(DuckDB 클래스), 그 인스턴스에 대한 여러 Connection, 그리고 쿼리 결과인 QueryResult 인스턴스라는 추상화를 중심으로 만들어져요. C++ API의 헤더 파일은 duckdb.hpp예요.
시작과 종료
DuckDB를 사용하려면 먼저 생성자로 DuckDB 인스턴스를 초기화해야 해요. DuckDB()는 읽고 쓸 데이터베이스 파일을 파라미터로 받아요. 특별한 값 nullptr을 사용하면 인메모리 데이터베이스를 만들 수 있어요. 참고로 인메모리 데이터베이스는 디스크에 아무것도 저장되지 않으니(즉 프로세스가 종료되면 모든 데이터가 사라져요) 이 점을 기억하세요. DuckDB 생성자의 두 번째 파라미터는 선택적인 DBConfig 객체예요. DBConfig에서는 읽기/쓰기 모드나 메모리 한도 같은 다양한 데이터베이스 파라미터를 설정할 수 있어요. DuckDB 생성자는 예를 들어 데이터베이스 파일을 사용할 수 없을 때 예외를 던질 수 있어요.
DuckDB 인스턴스가 있으면 Connection() 생성자로 하나 또는 여러 개의 Connection 인스턴스를 만들 수 있어요. 커넥션은 스레드 안전해야 하지만 쿼리 중에는 잠길 수 있어요. 따라서 멀티스레드 환경이라면 각 스레드가 자신만의 커넥션을 쓰는 걸 권장해요.
DuckDB db(nullptr);
Connection con(db);
쿼리
커넥션은 C++에서 DuckDB로 SQL 쿼리 문자열을 보내는 Query() 메서드를 노출해요. Query()는 반환 전에 쿼리 결과를 메모리의 MaterializedQueryResult로 완전히 실체화하며, 그 시점부터 쿼리 결과를 소비할 수 있어요. 쿼리용 스트리밍 API도 있는데, 아래에서 자세히 볼게요.
// create a table
con.Query("CREATE TABLE integers (i INTEGER, j INTEGER)");
// insert three rows into the table
con.Query("INSERT INTO integers VALUES (3, 4), (5, 6), (7, NULL)");
auto result = con.Query("SELECT * FROM integers");
if (result->HasError()) {
cerr << result->GetError() << endl;
} else {
cout << result->ToString() << endl;
}
MaterializedQueryResult 인스턴스에는 먼저 쿼리가 성공했는지 나타내는 필드 두 개가 포함돼요. Query는 정상적인 상황에서 예외를 던지지 않아요. 대신 잘못된 쿼리나 다른 문제는 쿼리 결과 인스턴스의 success Boolean 필드를 false로 설정해요. 이 경우 error에 오류 메시지(문자열)가 있을 수 있어요. GetErrorType()과 GetErrorObject() 메서드는 모든 QueryResult 인스턴스에서 사용할 수 있으며, 더 명시적인 오류 처리를 돕는 데 유용해요.
auto result = con.Query("INSERT INTO integers VALUES (1, 2)");
if (result->HasError()) {
auto errorType = result->GetErrorType();
switch (errorType) {
case duckdb::ExceptionType::CONSTRAINT: {
// Example handling
auto errorObject = result->GetErrorObject();
errorObject.ConvertErrorToJSON();
std::cout << errorObject.Message() << std::endl;
break;
}
// More handling
}
} else {
// Normal code
}
성공하면 다른 필드도 설정돼요. 방금 실행한 문의 타입(예: StatementType::INSERT_STATEMENT)이 statement_type에 들어 있어요. 결과 집합 열의 상위 수준("Logical type"/"SQL type") 타입은 types에 있어요. 결과 열의 이름은 names 문자열 벡터에 있어요. 결과 집합이 여러 개 반환되면(예: 결과 집합에 여러 문이 포함된 경우) next 필드로 결과 집합을 연결할 수 있어요.
DuckDB는 C++ API에서 Prepare() 메서드로 준비된 문도 지원해요. 이 메서드는 PreparedStatement 인스턴스를 반환해요. 이 인스턴스로 파라미터가 있는 준비된 문을 실행할 수 있어요. 예시는 아래와 같아요.
std::unique_ptr<PreparedStatement> prepare = con.Prepare("SELECT count(*) FROM a WHERE i = $1");
std::unique_ptr<QueryResult> result = prepare->Execute(12);
Warning 대량의 데이터를 DuckDB에 삽입하는 데 준비된 문을 사용하지 마세요. 더 나은 옵션은 [data import documentation]({% link docs/current/data/overview.md %})을 참고하세요.
UDF API
UDF API는 사용자 정의 함수를 정의할 수 있게 해줘요. duckdb:Connection에서 CreateScalarFunction(), CreateVectorizedFunction() 및 그 변형 메서드를 통해 노출돼요. 이 메서드들은 소유자 커넥션의 임시 스키마(TEMP_SCHEMA)에 UDF를 만들어요. 소유자 커넥션만 그것들을 사용하고 변경할 수 있어요.
CreateScalarFunction
사용자는 일반 스칼라 함수를 코딩하고 CreateScalarFunction()을 호출해 등록한 뒤 SELECT 문에서 UDF를 사용할 수 있어요. 예를 들어:
bool bigger_than_four(int value) {
return value > 4;
}
connection.CreateScalarFunction<bool, int>("bigger_than_four", &bigger_than_four);
connection.Query("SELECT bigger_than_four(i) FROM (VALUES (3), (5)) tbl(i)")->Print();
CreateScalarFunction() 메서드는 벡터화된 스칼라 UDF를 자동으로 만들어 내장 함수만큼 효율적으로 만들어요. 이 메서드 인터페이스에는 두 가지 변형이 있어요.
1.
template<typename TR, typename... Args>
void CreateScalarFunction(string name, TR (*udf_func)(Args…))
- 템플릿 파라미터:
- TR은 UDF 함수의 반환 타입.
- Args는 UDF 함수의 인자(최대 3개. 이 메서드는 삼항 함수까지만 지원).
- name은 UDF 함수를 등록할 이름.
- udf_func은 UDF 함수에 대한 포인터.
이 메서드는 템플릿 타입 이름에서 해당 LogicalType을 자동으로 발견해요.
bool→LogicalType::BOOLEANint8_t→LogicalType::TINYINTint16_t→LogicalType::SMALLINTint32_t→LogicalType::INTEGERint64_t→LogicalType::BIGINTfloat→LogicalType::FLOATdouble→LogicalType::DOUBLEstring_t→LogicalType::VARCHAR
DuckDB에서는 int32_t 같은 일부 원시 타입이 같은 LogicalType(INTEGER, TIME, DATE)으로 매핑돼요. 그럴 때 구분을 위해 사용자는 다음 오버로드 메서드를 사용할 수 있어요.
2.
template<typename TR, typename... Args>
void CreateScalarFunction(string name, vector<LogicalType> args, LogicalType ret_type, TR (*udf_func)(Args…))
사용 예시는:
int32_t udf_date(int32_t a) {
return a;
}
con.Query("CREATE TABLE dates (d DATE)");
con.Query("INSERT INTO dates VALUES ('1992-01-01')");
con.CreateScalarFunction<int32_t, int32_t>("udf_date", {LogicalType::DATE}, LogicalType::DATE, &udf_date);
con.Query("SELECT udf_date(d) FROM dates")->Print();
- 템플릿 파라미터:
- TR은 UDF 함수의 반환 타입.
- Args는 UDF 함수의 인자(최대 3개. 이 메서드는 삼항 함수까지만 지원).
- name은 UDF 함수를 등록할 이름.
- args는 함수가 사용하는 LogicalType 인자들. 템플릿 Args 타입과 일치해야 해요.
- ret_type은 함수 반환의 LogicalType. 템플릿 TR 타입과 일치해야 해요.
- udf_func은 UDF 함수에 대한 포인터.
이 함수는 템플릿 타입을 인자로 전달된 LogicalType과 비교해 다음과 같이 일치해야 해요.
LogicalTypeId::BOOLEAN→boolLogicalTypeId::TINYINT→int8_tLogicalTypeId::SMALLINT→int16_tLogicalTypeId::DATE,LogicalTypeId::TIME,LogicalTypeId::INTEGER→int32_tLogicalTypeId::BIGINT,LogicalTypeId::TIMESTAMP→int64_tLogicalTypeId::FLOAT,LogicalTypeId::DOUBLE,LogicalTypeId::DECIMAL→doubleLogicalTypeId::VARCHAR,LogicalTypeId::CHAR,LogicalTypeId::BLOB→string_tLogicalTypeId::VARBINARY→blob_t
CreateVectorizedFunction
CreateVectorizedFunction() 메서드는 다음과 같은 벡터화된 UDF를 등록해요.
/*
* This vectorized function copies the input values to the result vector
*/
template<typename TYPE>
static void udf_vectorized(DataChunk &args, ExpressionState &state, Vector &result) {
// set the result vector type
result.vector_type = VectorType::FLAT_VECTOR;
// get a raw array from the result
auto result_data = FlatVector::GetData<TYPE>(result);
// get the solely input vector
auto &input = args.data[0];
// now get an orrified vector
VectorData vdata;
input.Orrify(args.size(), vdata);
// get a raw array from the orrified input
auto input_data = (TYPE *)vdata.data;
// handling the data
for (idx_t i = 0; i < args.size(); i++) {
auto idx = vdata.sel->get_index(i);
if ((*vdata.nullmask)[idx]) {
continue;
}
result_data[i] = input_data[idx];
}
}
con.Query("CREATE TABLE integers (i INTEGER)");
con.Query("INSERT INTO integers VALUES (1), (2), (3), (999)");
con.CreateVectorizedFunction<int, int>("udf_vectorized_int", &&udf_vectorized<int>);
con.Query("SELECT udf_vectorized_int(i) FROM integers")->Print();
벡터화된 UDF는 _scalar_function_t_ 타입의 포인터예요.
typedef std::function<void(DataChunk &args, ExpressionState &expr, Vector &result)> scalar_function_t;
- args는 UDF를 위한 일련의 입력 벡터를 담는 DataChunk로, 모두 같은 길이를 가져요.
- expr은 쿼리의 표현식 상태에 정보를 제공하는 ExpressionState.
- result는 결과 값을 저장하는 Vector.
벡터화된 UDF에서 처리해야 할 다양한 벡터 타입이 있어요.
- ConstantVector
- DictionaryVector
- FlatVector
- ListVector
- StringVector
- StructVector
- SequenceVector
CreateVectorizedFunction() 메서드의 일반 API는 다음과 같아요.
1.
template<typename TR, typename... Args>
void CreateVectorizedFunction(string name, scalar_function_t udf_func, LogicalType varargs = LogicalType::INVALID)
- 템플릿 파라미터:
- TR은 UDF 함수의 반환 타입.
- Args는 UDF 함수의 인자(최대 3개).
- name은 UDF 함수를 등록할 이름.
- udf_func은 벡터화된 UDF 함수.
- varargs — 지원할 varargs의 타입, 또는 함수가 가변 길이 인자를 받지 않으면
LogicalTypeId::INVALID(기본값).
이 메서드는 템플릿 타입 이름에서 해당 LogicalType을 자동으로 발견해요.
bool→LogicalType::BOOLEANint8_t→LogicalType::TINYINTint16_t→LogicalType::SMALLINTint32_t→LogicalType::INTEGERint64_t→LogicalType::BIGINTfloat→LogicalType::FLOATdouble→LogicalType::DOUBLEstring_t→LogicalType::VARCHAR
2.
template<typename TR, typename... Args>
void CreateVectorizedFunction(string name, vector<LogicalType> args, LogicalType ret_type, scalar_function_t udf_func, LogicalType varargs = LogicalType::INVALID)