blob: 9c7ae75f8f7af9cbce443c098bbb4cf6d25f781a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// RECIPE STARTS HERE
/// Here we'll show the structure of building an ADBC driver in C++ using
/// the ADBC driver framework library. This is the same library that ADBC
/// uses to build its SQLite and PostgreSQL drivers and abstracts away
/// the details of C callables and catalog/metadata functions that can be
/// difficult to implement but are essential for efficiently leveraging
/// the rest of the ADBC ecosystem.
///
/// At a high level, we'll be building a driver whose "database" is a directory
/// where each "table" in the database is a file containing an Arrow IPC stream.
/// Tables can be written using the bulk ingest feature and tables can be read
/// with a simple query in the form ``SELECT * FROM (the file)``.
///
/// 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:
///
/// .. code-block:: shell
///
/// mamba install cmake compilers libadbc-driver-manager
///
/// .. _conda-forge: https://conda-forge.org/
///
/// Building
/// ========
///
/// We'll use CMake_ here. From a source checkout of the ADBC repository:
///
/// .. code-block:: shell
///
/// mkdir build
/// cd build
/// cmake ../docs/source/cpp/recipe_driver -DADBC_DRIVER_EXAMPLE_BUILD_TESTS=ON
/// cmake --build .
/// ctest
///
/// .. _CMake: https://cmake.org/
///
/// Building an ADBC Driver using C++
/// =================================
///
/// Let's start with some includes. Notably, we'll need the driver framework
/// header files and nanoarrow_, which we'll use to create and consume the
/// Arrow C data interface structures in this example driver.
/// .. _nanoarrow: https://arrow.apache.org/nanoarrow
#include "driver_example.h"
#include <cstdio>
#include <string>
#include "driver/framework/connection.h"
#include "driver/framework/database.h"
#include "driver/framework/statement.h"
#include "nanoarrow/nanoarrow.hpp"
#include "nanoarrow/nanoarrow_ipc.hpp"
#include "arrow-adbc/adbc.h"
/// Next, we'll bring a few essential framework types into the namespace
/// to reduce the verbosity of the implementation:
///
/// * :cpp:class:`adbc::driver::Option` : Options can be set on an ADBC database,
/// connection, and statmenent. They can be strings, opaque binary, doubles, or
/// integers. The ``Option`` class abstracts the details of how to get, set,
/// and parse these values.
/// * :cpp:class:`adbc::driver::Status`: The ``Status`` is the ADBC driver
/// framework's error handling mechanism: functions with no return value that
/// can fail return a ``Status``. You can use ``UNWRAP_STATUS(some_call())`` as
/// shorthand for ``Status status = some_call(); if (!status.ok()) return
/// status;`` to succinctly propagate errors.
/// * :cpp:class:`adbc::driver::Result`: The ``Result<T>`` is used as a return
/// value for functions that on success return a value of type ``T`` and on
/// failure communicate their error using a ``Status``. You can use
/// ``UNWRAP_RESULT(some_type value, some_call())`` as shorthand for
///
/// .. code-block:: cpp
///
/// some_type value;
/// Result<some_type> maybe_value = some_call();
/// if (!maybe_value.status().ok()) {
/// return maybe_value.status();
/// } else {
/// value = *maybe_value;
/// }
using adbc::driver::Option;
using adbc::driver::Result;
using adbc::driver::Status;
namespace {
/// Next, we'll provide the database implementation. The driver framework uses
/// the Curiously Recurring Template Pattern (CRTP_). The details of this are
/// handled by the framework, but functionally this is still just overriding
/// methods from a base class that handles the details.
///
/// Here, our database implementation will simply record the ``uri`` passed
/// by the user. Our interpretation of this will be a ``file://`` uri to
/// a directory to which our IPC files should be written and/or IPC files
/// should be read. This is the role of the database in ADBC: a shared
/// handle to a database that potentially caches some shared state among
/// connections, but which still allows multiple connections to execute
/// against the database concurrently.
///
/// .. _CRTP: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
class DriverExampleDatabase : public adbc::driver::Database<DriverExampleDatabase> {
public:
[[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[example]";
Status SetOptionImpl(std::string_view key, Option value) override {
// Handle and validate options implemented by this driver
if (key == "uri") {
UNWRAP_RESULT(std::string_view uri, value.AsString());
if (uri.find("file://") != 0) {
return adbc::driver::status::InvalidArgument(
"[example] uri must start with 'file://'");
}
uri_ = uri;
return adbc::driver::status::Ok();
}
// Defer to the base implementation to handle state managed by the base
// class (and error for all other options).
return Base::SetOptionImpl(key, value);
}
Result<Option> GetOption(std::string_view key) override {
// Return the value of options implemented by this driver
if (key == "uri") {
return Option(uri_);
}
// Defer to the base implementation to handle state managed by the base
// class (and error for all other options).
return Base::GetOption(key);
}
// This is called after zero or more calls to SetOption() on
Status InitImpl() override {
if (uri_.empty()) {
return adbc::driver::status::InvalidArgument(
"[example] Must set uri to a non-empty value");
}
return Base::InitImpl();
}
// Getters for members needed by the connection and/or statement:
const std::string& uri() { return uri_; }
private:
std::string uri_;
};
/// Next, we implement the connection. While the role of the database is typically
/// to store or cache information, the role of the connection is to provide
/// resource handles that might be expensive to obtain (e.g., negotiating authentication
/// when connecting to a database). Because our example "database" is just a directory, we
/// don't need to do much in our connection in terms of resource management except to
/// provide a way for child statements to access the database's uri.
///
/// Another role of the connection is to provide metadata about tables, columns,
/// statistics, and other catalog-like information a caller might want to know before
/// issuing a query. The driver framework base classes provide helpers to implement these
/// functions such that you can mostly implement them in terms of the C++17 standard
/// library (as opposed to building the C-level arrays yourself).
class DriverExampleConnection : public adbc::driver::Connection<DriverExampleConnection> {
public:
[[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[example]";
// Get information from the database and/or store a reference if needed.
Status InitImpl(void* parent) {
auto& database = *reinterpret_cast<DriverExampleDatabase*>(parent);
uri_ = database.uri();
return Base::InitImpl(parent);
}
// Getters for members needed by the statement:
const std::string& uri() { return uri_; }
private:
std::string uri_;
};
/// Next, we provide the statement implementation. The statement is where query execution
/// is managed. Because our data source is quite literally Arrow data, we don't have to
/// provide a layer that manages type or value conversion. The SQLite and PostgreSQL
/// drivers both dedicate many lines of code to implementing and testing these conversions
/// efficiently. The nanoarrow library can be used to implement conversions in both
/// directions and is the scope of a separate article.
class DriverExampleStatement : public adbc::driver::Statement<DriverExampleStatement> {
public:
[[maybe_unused]] constexpr static std::string_view kErrorPrefix = "[example]";
// Get information from the connection and/or store a reference if needed.
Status InitImpl(void* parent) {
auto& connection = *reinterpret_cast<DriverExampleConnection*>(parent);
uri_ = connection.uri();
return Base::InitImpl(parent);
}
// Our implementation of a bulk ingestion is to write an Arrow IPC stream as a file
// using the target table as the filename.
Result<int64_t> ExecuteIngestImpl(IngestState& state) {
std::string directory = uri_.substr(strlen("file://"));
std::string filename = directory + "/" + *state.target_table;
nanoarrow::ipc::UniqueOutputStream output_stream;
FILE* c_file = std::fopen(filename.c_str(), "wb");
UNWRAP_ERRNO(Internal, ArrowIpcOutputStreamInitFile(output_stream.get(), c_file,
/*close_on_release*/ true));
nanoarrow::ipc::UniqueWriter writer;
UNWRAP_ERRNO(Internal, ArrowIpcWriterInit(writer.get(), output_stream.get()));
ArrowError nanoarrow_error;
ArrowErrorInit(&nanoarrow_error);
UNWRAP_NANOARROW(nanoarrow_error, Internal,
ArrowIpcWriterWriteArrayStream(writer.get(), &bind_parameters_,
&nanoarrow_error));
return -1;
}
// Our implementation of query execution is to accept a simple query in the form
// SELECT * FROM (the filename).
Result<int64_t> ExecuteQueryImpl(QueryState& state, ArrowArrayStream* stream) {
std::string prefix("SELECT * FROM ");
if (state.query.find(prefix) != 0) {
return adbc::driver::status::InvalidArgument(
"[example] Query must be in the form 'SELECT * FROM filename'");
}
std::string directory = uri_.substr(strlen("file://"));
std::string filename = directory + "/" + state.query.substr(prefix.size());
nanoarrow::ipc::UniqueInputStream input_stream;
FILE* c_file = std::fopen(filename.c_str(), "rb");
UNWRAP_ERRNO(Internal, ArrowIpcInputStreamInitFile(input_stream.get(), c_file,
/*close_on_release*/ true));
UNWRAP_ERRNO(Internal,
ArrowIpcArrayStreamReaderInit(stream, input_stream.get(), nullptr));
return -1;
}
// This path is taken when the user calls Prepare() first.
Result<int64_t> ExecuteQueryImpl(PreparedState& state, ArrowArrayStream* stream) {
QueryState query_state{state.query};
return ExecuteQueryImpl(query_state, stream);
}
private:
std::string uri_;
};
} // namespace
/// Finally, we create the driver initializer function, which is what the driver
/// manager needs to provide implementations for the ``Adbc**()`` functions that
/// comprise the ADBC C API. The name of this function matters: this file will
/// be built into a shared library named ``libdriver_example.(so|dll|dylib)``,
/// so the driver manager will look for the symbol ``AdbcDriverExampleInit()``
/// as the default entry point when asked to load the driver ``"driver_example"``.
extern "C" AdbcStatusCode AdbcDriverExampleInit(int version, void* raw_driver,
AdbcError* error) {
using ExampleDriver =
adbc::driver::Driver<DriverExampleDatabase, DriverExampleConnection,
DriverExampleStatement>;
return ExampleDriver::Init(version, raw_driver, error);
}