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을 자동으로 발견해요.

  • boolLogicalType::BOOLEAN
  • int8_tLogicalType::TINYINT
  • int16_tLogicalType::SMALLINT
  • int32_tLogicalType::INTEGER
  • int64_tLogicalType::BIGINT
  • floatLogicalType::FLOAT
  • doubleLogicalType::DOUBLE
  • string_tLogicalType::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::BOOLEANbool
  • LogicalTypeId::TINYINTint8_t
  • LogicalTypeId::SMALLINTint16_t
  • LogicalTypeId::DATE, LogicalTypeId::TIME, LogicalTypeId::INTEGERint32_t
  • LogicalTypeId::BIGINT, LogicalTypeId::TIMESTAMPint64_t
  • LogicalTypeId::FLOAT, LogicalTypeId::DOUBLE, LogicalTypeId::DECIMALdouble
  • LogicalTypeId::VARCHAR, LogicalTypeId::CHAR, LogicalTypeId::BLOBstring_t
  • LogicalTypeId::VARBINARYblob_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을 자동으로 발견해요.

  • boolLogicalType::BOOLEAN
  • int8_tLogicalType::TINYINT
  • int16_tLogicalType::SMALLINT
  • int32_tLogicalType::INTEGER
  • int64_tLogicalType::BIGINT
  • floatLogicalType::FLOAT
  • doubleLogicalType::DOUBLE
  • string_tLogicalType::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)

더 알아보기 (Learn more)