Quickstart

Recipe source: quickstart.cc

Here we’ll briefly tour basic features of ADBC with the SQLite driver in C++17.

Installation

This quickstart is actually a literate C++ file. You can clone the repository, build the sample, and follow along.

We’ll assume you’re using conda-forge for dependencies. CMake, a C++17 compiler, and the ADBC libraries are required. They can be installed as follows:

mamba install cmake compilers libadbc-driver-manager

Installing Drivers

See Installing Drivers for instructions on installing ADBC drivers for the database you want to connect to. For the example below, you could install dbc and then use dbc to install the SQLite driver and its manifest in the user configuration directory where the driver manager library will search for it:

dbc install --level user sqlite

You can also build drivers from source or use other installation methods. See the driver documentation for more details.

Building

We’ll use CMake here. From a source checkout of the ADBC repository:

mkdir build
cd build
cmake ../docs/source/cpp/recipe
cmake --build . --target recipe-quickstart
./recipe-quickstart

Basic Example

Let’s start with some includes:

78// For EXIT_SUCCESS
79#include <cstdlib>
80// For strerror
81#include <cstring>
82#include <iostream>
83
84#include <arrow-adbc/adbc.h>
85#include <arrow-adbc/adbc_driver_manager.h>
86#include <nanoarrow.h>

Then we’ll add some (very basic) error checking helpers.

 90// Error-checking helper for ADBC calls.
 91// Assumes that there is an AdbcError named `error` in scope.
 92#define CHECK_ADBC(EXPR)                                          \
 93  if (AdbcStatusCode status = (EXPR); status != ADBC_STATUS_OK) { \
 94    if (error.message != nullptr) {                               \
 95      std::cerr << error.message << std::endl;                    \
 96    }                                                             \
 97    return EXIT_FAILURE;                                          \
 98  }
 99
100// Error-checking helper for ArrowArrayStream.
101#define CHECK_STREAM(STREAM, EXPR)                            \
102  if (int status = (EXPR); status != 0) {                     \
103    std::cerr << "(" << std::strerror(status) << "): ";       \
104    const char* message = (STREAM).get_last_error(&(STREAM)); \
105    if (message != nullptr) {                                 \
106      std::cerr << message << std::endl;                      \
107    } else {                                                  \
108      std::cerr << "(no error message)" << std::endl;         \
109    }                                                         \
110    return EXIT_FAILURE;                                      \
111  }
112
113// Error-checking helper for Nanoarrow.
114#define CHECK_NANOARROW(EXPR)                                              \
115  if (int status = (EXPR); status != 0) {                                  \
116    std::cerr << "(" << std::strerror(status) << "): failed" << std::endl; \
117    return EXIT_FAILURE;                                                   \
118  }
119
120int main() {

Loading the Driver

We’ll load the SQLite driver using the driver manager. We don’t have to explicitly link to the driver this way.

127  AdbcError error = {};
128
129  AdbcDatabase database = {};
130  CHECK_ADBC(AdbcDatabaseNew(&database, &error));

The way the driver manager knows what driver we want is via the driver option.

133  CHECK_ADBC(AdbcDatabaseSetOption(&database, "driver", "sqlite", &error));

We also need to enable searching for driver manifests.

135  CHECK_ADBC(
136      AdbcDriverManagerDatabaseSetLoadFlags(&database, ADBC_LOAD_FLAG_DEFAULT, &error));
137  CHECK_ADBC(AdbcDatabaseInit(&database, &error));

Creating a Connection

ADBC distinguishes between “databases”, “connections”, and “statements”. A “database” holds shared state across multiple connections. For example, in the SQLite driver, it holds the actual instance of SQLite. A “connection” is one connection to the database.

148  AdbcConnection connection = {};
149  CHECK_ADBC(AdbcConnectionNew(&connection, &error));
150  CHECK_ADBC(AdbcConnectionInit(&connection, &database, &error));

Creating a Statement

A statement lets us execute queries. They are used for both prepared and non-prepared (“ad-hoc”) queries.

158  AdbcStatement statement = {};
159  CHECK_ADBC(AdbcStatementNew(&connection, &statement, &error));

Executing a Query

We execute a query by setting the query on the statement, then calling AdbcStatementExecuteQuery(). The results come back through the Arrow C Data Interface.

170  struct ArrowArrayStream stream = {};
171  int64_t rows_affected = -1;
172
173  CHECK_ADBC(AdbcStatementSetSqlQuery(&statement, "SELECT 42 AS THEANSWER", &error));
174  CHECK_ADBC(AdbcStatementExecuteQuery(&statement, &stream, &rows_affected, &error));

While the API gives us the number of rows, the SQLite driver can’t actually know how many rows there are in the result set ahead of time, so this value will actually just be -1 to indicate that the value is not known.

180  std::cout << "Got " << rows_affected << " rows" << std::endl;

We need an Arrow implementation to read the actual results. We can use Arrow C++ or Nanoarrow for that. For simplicity, we’ll use Nanoarrow here. (The CMake configuration for this example downloads and builds Nanoarrow from source as part of the build.)

First we’ll get the schema of the data:

193  ArrowSchema schema = {};
194  CHECK_STREAM(stream, stream.get_schema(&stream, &schema));

Then we can use Nanoarrow to print it:

197  char buf[1024] = {};
198  ArrowSchemaToString(&schema, buf, sizeof(buf), /*recursive=*/1);
199  std::cout << "Result schema: " << buf << std::endl;

Now we can read the data. The data comes as a stream of Arrow record batches.

205  while (true) {
206    ArrowArray batch = {};
207    CHECK_STREAM(stream, stream.get_next(&stream, &batch));
208
209    if (batch.release == nullptr) {
210      // Stream has ended
211      break;
212    }

We can use Nanoarrow to print out the data, too.

215    ArrowArrayView view = {};
216    CHECK_NANOARROW(ArrowArrayViewInitFromSchema(&view, &schema, nullptr));
217    CHECK_NANOARROW(ArrowArrayViewSetArray(&view, &batch, nullptr));
218    std::cout << "Got a batch with " << batch.length << " rows" << std::endl;
219    for (int64_t i = 0; i < batch.length; i++) {
220      std::cout << "THEANSWER[" << i
221                << "] = " << view.children[0]->buffer_views[1].data.as_int64[i]
222                << std::endl;
223    }
224    ArrowArrayViewReset(&view);
225  }
226  // Output:
227  // Got a batch with 1 rows
228  // THEANSWER[0] = 42
229
230  stream.release(&stream);

Cleanup

At the end, we must release all our resources.

236  CHECK_ADBC(AdbcStatementRelease(&statement, &error));
237  CHECK_ADBC(AdbcConnectionRelease(&connection, &error));
238  CHECK_ADBC(AdbcDatabaseRelease(&database, &error));
239  return EXIT_SUCCESS;
240}
stdout
Got -1 rows
Result schema: struct<THEANSWER: int64>
Got a batch with 1 rows
THEANSWER[0] = 42