The DuckDB C API provides functions for executing SQL queries and handling results.
Basic Query Execution
Simple Queries
Execute a SQL query and retrieve results:
duckdb_state duckdb_query (
duckdb_connection connection ,
const char * query ,
duckdb_result * out_result
);
connection
duckdb_connection
required
Active database connection.
SQL query string to execute.
Output parameter for query results. Pass NULL if you don’t need results (e.g., for DDL statements).
Returns : DuckDBSuccess on success, DuckDBError on failure.
DDL Statement
DML Statement
SELECT Query
// No result needed
if ( duckdb_query (con, "CREATE TABLE test(i INTEGER)" , NULL ) == DuckDBError) {
fprintf (stderr, "Failed to create table \n " );
}
Working with Results
Result Structure
The duckdb_result struct contains query results:
typedef struct {
idx_t column_count; // Internal: use duckdb_column_count()
idx_t row_count; // Internal: use duckdb_row_count()
idx_t rows_changed; // Internal: use duckdb_rows_changed()
// ... other internal fields
} duckdb_result;
Never access duckdb_result fields directly. Always use the provided accessor functions.
Get information about query results:
// Get number of columns
idx_t duckdb_column_count (duckdb_result * result );
// Get number of rows
idx_t duckdb_row_count (duckdb_result * result );
// Get column name
const char * duckdb_column_name (duckdb_result * result , idx_t col );
// Get column type
duckdb_type duckdb_column_type (duckdb_result * result , idx_t col );
// Get number of rows affected (for INSERT/UPDATE/DELETE)
idx_t duckdb_rows_changed (duckdb_result * result );
Example :
duckdb_result result;
if ( duckdb_query (con, "SELECT * FROM integers" , & result ) == DuckDBSuccess) {
idx_t col_count = duckdb_column_count ( & result);
idx_t row_count = duckdb_row_count ( & result);
// Print column headers
for ( idx_t i = 0 ; i < col_count; i ++ ) {
printf ( " %s \t " , duckdb_column_name ( & result, i));
}
printf ( " \n " );
duckdb_destroy_result ( & result);
}
Accessing Values
Extract values from results:
// Generic varchar extraction (works for all types)
char * duckdb_value_varchar (duckdb_result * result , idx_t col , idx_t row );
// Type-specific extraction
bool duckdb_value_boolean (duckdb_result * result , idx_t col , idx_t row );
int8_t duckdb_value_int8 (duckdb_result * result , idx_t col , idx_t row );
int16_t duckdb_value_int16 (duckdb_result * result , idx_t col , idx_t row );
int32_t duckdb_value_int32 (duckdb_result * result , idx_t col , idx_t row );
int64_t duckdb_value_int64 (duckdb_result * result , idx_t col , idx_t row );
float duckdb_value_float (duckdb_result * result , idx_t col , idx_t row );
double duckdb_value_double (duckdb_result * result , idx_t col , idx_t row );
duckdb_date duckdb_value_date (duckdb_result * result , idx_t col , idx_t row );
duckdb_time duckdb_value_time (duckdb_result * result , idx_t col , idx_t row );
duckdb_timestamp duckdb_value_timestamp (duckdb_result * result , idx_t col , idx_t row );
// Check for NULL
bool duckdb_value_is_null (duckdb_result * result , idx_t col , idx_t row );
duckdb_value_varchar() returns a malloc-allocated string that must be freed with duckdb_free().
Complete example :
duckdb_result result;
if ( duckdb_query (con, "SELECT name, age, salary FROM employees" , & result ) == DuckDBSuccess) {
idx_t row_count = duckdb_row_count ( & result);
idx_t col_count = duckdb_column_count ( & result);
for ( idx_t row = 0 ; row < row_count; row ++ ) {
// Check for NULL
if ( duckdb_value_is_null ( & result, 0 , row)) {
printf ( "NULL \t " );
} else {
char * name = duckdb_value_varchar ( & result, 0 , row);
printf ( " %s \t " , name);
duckdb_free (name);
}
// Integer column
int32_t age = duckdb_value_int32 ( & result, 1 , row);
printf ( " %d \t " , age);
// Double column
double salary = duckdb_value_double ( & result, 2 , row);
printf ( " %.2f \n " , salary);
}
duckdb_destroy_result ( & result);
}
Error Handling
Check for query errors:
const char * duckdb_result_error (duckdb_result * result );
Returns : Error message if the query failed, NULL if successful.
duckdb_result result;
if ( duckdb_query (con, "SELECT * FROM nonexistent_table" , & result ) == DuckDBError) {
const char * error = duckdb_result_error ( & result);
if (error) {
fprintf (stderr, "Query error: %s \n " , error);
}
duckdb_destroy_result ( & result);
}
Destroying Results
Always free result resources:
void duckdb_destroy_result (duckdb_result * result );
Failing to call duckdb_destroy_result() causes memory leaks. Call it even if the query failed.
Data Chunks (Advanced)
For better performance with large result sets, access data chunks directly:
duckdb_data_chunk duckdb_result_get_chunk (duckdb_result result , idx_t chunk_index );
idx_t duckdb_result_chunk_count (duckdb_result result );
This avoids materialization overhead and processes data in vectorized batches.
Statement Types
Check the type of statement executed:
duckdb_statement_type duckdb_result_statement_type (duckdb_result result );
Returns one of:
DUCKDB_STATEMENT_TYPE_SELECT
DUCKDB_STATEMENT_TYPE_INSERT
DUCKDB_STATEMENT_TYPE_UPDATE
DUCKDB_STATEMENT_TYPE_DELETE
DUCKDB_STATEMENT_TYPE_CREATE
etc.
Complete Example
Full example with error handling:
#include "duckdb.h"
#include <stdio.h>
int main () {
duckdb_database db;
duckdb_connection con;
duckdb_result result;
// Open database and create connection
if ( duckdb_open ( NULL , & db) == DuckDBError ||
duckdb_connect (db, & con) == DuckDBError) {
fprintf (stderr, "Failed to open database \n " );
return 1 ;
}
// Create table
if ( duckdb_query (con, "CREATE TABLE test(id INTEGER, name VARCHAR)" , NULL ) == DuckDBError) {
fprintf (stderr, "Failed to create table \n " );
goto cleanup;
}
// Insert data
const char * insert_sql = "INSERT INTO test VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie')" ;
if ( duckdb_query (con, insert_sql, NULL ) == DuckDBError) {
fprintf (stderr, "Failed to insert data \n " );
goto cleanup;
}
// Query data
if ( duckdb_query (con, "SELECT * FROM test WHERE id > 1" , & result) == DuckDBError) {
const char * error = duckdb_result_error ( & result);
fprintf (stderr, "Query failed: %s \n " , error ? error : "unknown error" );
duckdb_destroy_result ( & result);
goto cleanup;
}
// Print results
idx_t row_count = duckdb_row_count ( & result);
idx_t col_count = duckdb_column_count ( & result);
// Headers
for ( idx_t i = 0 ; i < col_count; i ++ ) {
printf ( " %s \t " , duckdb_column_name ( & result, i));
}
printf ( " \n " );
// Data
for ( idx_t row = 0 ; row < row_count; row ++ ) {
for ( idx_t col = 0 ; col < col_count; col ++ ) {
if ( duckdb_value_is_null ( & result, col, row)) {
printf ( "NULL \t " );
} else {
char * val = duckdb_value_varchar ( & result, col, row);
printf ( " %s \t " , val);
duckdb_free (val);
}
}
printf ( " \n " );
}
duckdb_destroy_result ( & result);
cleanup:
duckdb_disconnect ( & con);
duckdb_close ( & db);
return 0 ;
}
Next Steps
Prepared Statements Use parameterized queries for better performance and security
Appender Bulk load data efficiently
Data Types Work with complex data types