Files
libcifpp/include/cif++/cql.hpp
Maarten L. Hekkelman d5fbf05b44 documenting
2026-02-25 17:16:09 +01:00

507 lines
14 KiB
C++

/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2025 NKI/AVL, Netherlands Cancer Institute
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
/**
* @file cql.hpp
*
* This file contains code to access stored data as if it were
* a relation database. The underlying code uses SQLite as engine.
* categories are exposed as virtual tables.
*/
#include "cif++/category.hpp"
#include "cif++/item.hpp"
#include "cif++/iterator.hpp"
#include "cif++/row.hpp"
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <memory>
#include <ostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
// --------------------------------------------------------------------
namespace cif::cql
{
class connection;
struct result_impl;
// --------------------------------------------------------------------
/// Reference to a field in the result set
class field_ref final
{
public:
/// The name of the field
[[nodiscard]] std::string_view name() const &
{
return m_row.get_category().get_item_name(m_index);
}
/// The index number of the field
[[nodiscard]] constexpr size_t num() const noexcept
{
return m_index;
}
/** Return the contents of this item as type @tparam T */
template <typename T = std::string>
[[nodiscard]] auto get() const -> T
{
return m_row[m_index].get<T>();
}
/// Returns true if the field contains NULL
[[nodiscard]] bool is_null() const
{
return m_row[m_index].is_null();
}
/** Return the contents of this item as type @tparam T or, if not
* set, use @a dv as the default value.
*/
template <typename T>
auto value_or(const T &dv) const
{
return m_row[m_index].value_or(dv);
}
/// Constructor
field_ref(const_row_handle rh, uint16_t col, std::shared_ptr<result_impl> result_impl)
: m_row(std::move(rh))
, m_index(col)
, m_result_impl(std::move(result_impl))
{
}
/// Copy constructor
field_ref(const field_ref &) = default;
/// Move constructor
field_ref(field_ref &&) = default;
/// Copy assignment
field_ref &operator=(const field_ref &) = default;
/// Move assignment
field_ref &operator=(field_ref &&) = default;
private:
const_row_handle m_row;
uint16_t m_index;
std::shared_ptr<result_impl> m_result_impl;
};
// --------------------------------------------------------------------
/// A reference to a row in the result set
class row_ref final
{
public:
/// Iterator for the items in this row
class const_field_iterator
{
public:
friend class result;
/// @cond
using iterator_category = std::forward_iterator_tag;
using value_type = const field_ref;
using difference_type = std::ptrdiff_t;
using pointer = value_type *;
using reference = value_type &;
const_field_iterator(const const_field_iterator &) = default;
const_field_iterator(const_field_iterator &&) = default;
const_field_iterator &operator=(const const_field_iterator &) = default;
const_field_iterator &operator=(const_field_iterator &&) = default;
reference operator*()
{
return m_current;
}
pointer operator->()
{
return &m_current;
}
const_field_iterator &operator++()
{
if (m_row)
{
++m_col;
m_current = field_ref(m_row, m_col, m_result_impl);
}
return *this;
}
const_field_iterator operator++(int)
{
const_field_iterator result(*this);
this->operator++();
return result;
}
bool operator==(const const_field_iterator &rhs) const
{
return m_row == rhs.m_row and m_col == rhs.m_col;
}
bool operator!=(const const_field_iterator &rhs) const
{
return m_row != rhs.m_row or m_col != rhs.m_col;
}
private:
friend class row_ref;
const_field_iterator(const_row_handle row, uint16_t column, std::shared_ptr<result_impl> result_impl)
: m_row(std::move(row))
, m_col(column)
, m_current(m_row, m_col, result_impl)
, m_result_impl(result_impl)
{
}
const_row_handle m_row;
uint16_t m_col;
field_ref m_current;
std::shared_ptr<result_impl> m_result_impl;
/// @endcond
};
// --------------------------------------------------------------------
row_ref() = default;
/// Constructor
row_ref(const_row_handle rh, std::shared_ptr<result_impl> result_impl)
: m_row(std::move(rh))
, m_result_impl(std::move(result_impl))
{
}
/// @cond
row_ref(const row_ref &) = default;
row_ref &operator=(const row_ref &) = default;
/// @endcond
// --------------------------------------------------------------------
[[nodiscard]] const_field_iterator begin() const noexcept { return { m_row, 0, m_result_impl }; } ///< Return begin field iterator
[[nodiscard]] const_field_iterator cbegin() const noexcept { return { m_row, 0, m_result_impl }; } ///< Return cbegin field iterator
[[nodiscard]] const_field_iterator end() const noexcept { return { m_row, static_cast<uint16_t>(size()), m_result_impl }; } ///< Return end field iterator
[[nodiscard]] const_field_iterator cend() const noexcept { return { m_row, static_cast<uint16_t>(size()), m_result_impl }; } ///< Return cend field iterator
[[nodiscard]] field_ref front() const noexcept { return { m_row, 0, m_result_impl }; } ///< return reference to front field
[[nodiscard]] field_ref back() const noexcept { return { m_row, static_cast<uint16_t>(size() - 1), m_result_impl }; } ///< return reference to back field
[[nodiscard]] size_t size() const noexcept; ///< return number of items in the row
[[nodiscard]] bool empty() const noexcept { return size() == 0; } ///< return if the row contains no items at all
[[nodiscard]] field_ref operator[](uint16_t index) const noexcept { return { m_row, index, m_result_impl }; } ///< access field by index
[[nodiscard]] field_ref operator[](std::string_view name) const; ///< access field by name
// --------------------------------------------------------------------
/// @cond
bool operator==(const row_ref &rhs) const { return m_row == rhs.m_row; }
bool operator!=(const row_ref &rhs) const { return m_row != rhs.m_row; }
private:
const_row_handle m_row;
std::shared_ptr<result_impl> m_result_impl;
/// @endcond
};
// --------------------------------------------------------------------
/// The result set, containing the result of a query
class result
{
public:
// --------------------------------------------------------------------
/// iterator to the rows in the result set
class iterator
{
public:
friend class view;
/// @cond
using iterator_category = std::forward_iterator_tag;
using value_type = const row_ref;
using difference_type = std::ptrdiff_t;
using pointer = value_type *;
using reference = value_type &;
// const_row_iterator() = default;
iterator(std::shared_ptr<result_impl> result_impl, category::const_iterator cat_iter)
: m_iter(std::move(cat_iter))
, m_current(*m_iter, result_impl)
, m_result_impl(result_impl)
{
}
iterator(const iterator &) = default;
iterator(iterator &&) = default;
// const_row_iterator &operator=(const const_row_iterator &) = default;
// const_row_iterator &operator=(const_row_iterator &&) = default;
reference operator*()
{
return m_current;
}
pointer operator->()
{
return &m_current;
}
iterator &operator++()
{
++m_iter;
m_current = { *m_iter, m_result_impl };
return *this;
}
iterator operator++(int)
{
iterator result(*this);
this->operator++();
return result;
}
bool operator==(const iterator &rhs) const
{
return m_result_impl == rhs.m_result_impl and m_iter == rhs.m_iter;
}
bool operator!=(const iterator &rhs) const
{
return m_result_impl != rhs.m_result_impl or m_iter != rhs.m_iter;
}
private:
category::const_iterator m_iter;
row_ref m_current;
std::shared_ptr<result_impl> m_result_impl;
/// @endcond
};
// --------------------------------------------------------------------
/// @cond
result() = delete;
result(result const &rhs) noexcept = default;
result(result &&rhs) noexcept = default;
result &operator=(result const &rhs) noexcept = default;
result &operator=(result &&rhs) noexcept = default;
result(category &&data, const std::string &query = "");
~result() = default;
/// @endcond
/// Return the row if and only if the result set contains exactly one row, throws otherwise
[[nodiscard]] row_ref one_row() const
{
if (size() != 1)
throw std::runtime_error("Expected one row");
return front();
}
/// Return the row if and only if the result set contains exactly one row,
/// and this row also contains only one field, throws otherwise
[[nodiscard]] field_ref one_field() const
{
expect_columns(1);
if (size() != 1)
throw std::runtime_error("Expected one row");
return one_row().front();
}
// --------------------------------------------------------------------
/// @cond
[[nodiscard]] iterator begin() const noexcept;
[[nodiscard]] iterator cbegin() const noexcept;
[[nodiscard]] iterator end() const noexcept;
[[nodiscard]] iterator cend() const noexcept;
[[nodiscard]] row_ref front() const;
[[nodiscard]] row_ref back() const;
[[nodiscard]] size_t size() const noexcept;
[[nodiscard]] bool empty() const noexcept { return size() == 0; }
/// @endcond
/// Return the number of colums/fields in each row
[[nodiscard]] size_t column_count() const;
/// Return the result set as a cif::category
[[nodiscard]] category &get_category() const;
/// Test to see if the result set contains at least the number of fields/columns
/// but only when not empty
void expect_columns(size_t cols) const
{
if (auto actual = column_count(); size() > 0 and cols != actual)
throw std::runtime_error("Unexpected number of columns");
}
// --------------------------------------------------------------------
/// Print out the result set, for debugging mostly
friend std::ostream &operator<<(std::ostream &os, const result &r)
{
os << r.get_category();
return os;
}
private:
friend class transaction;
friend class SelectStatement;
std::shared_ptr<result_impl> m_impl;
};
// --------------------------------------------------------------------
/// Helper class to allow access to the data as a stream
template <typename... Ts>
class cql_iterator_proxy : public cif::iterator_proxy<Ts...>
{
public:
/// Constructor
cql_iterator_proxy(result &&res)
: cif::iterator_proxy<Ts...>(res.get_category())
, m_result(std::forward<result>(res))
{
m_result.expect_columns(cif::iterator_proxy<Ts...>::N);
}
private:
result m_result;
};
// --------------------------------------------------------------------
/// Transaction class.
/// At construction, this class starts a transaction on the connection
/// and at exit an automatic rollback is done, unless commit was called.
class transaction final
{
public:
/// Constructor
transaction(connection &conn);
/// @cond
~transaction();
transaction(const transaction &) = delete;
transaction &operator=(const transaction &) = delete;
/// @endcond
/// \brief Execute the sql in @a query returning an iterable result
result exec(std::string query);
/// \brief Execute the sql in @a query returning an iterable result.
/// Updates @a tail with what remains after the first statement in @a query
result exec(std::string query, std::string &tail);
/// Execute the sql in @a sql and return the result as a stream
template <typename... Ts>
cql_iterator_proxy<Ts...> stream(const std::string &sql)
{
return cql_iterator_proxy<Ts...>{ exec(sql) };
}
/// Commit the result of the operations
void commit();
/// Rollback the result of the operations, the underlying data is
/// restored to the state before the construction of this transaction.
void rollback();
private:
connection &m_conn;
bool m_transaction_active = false;
};
// --------------------------------------------------------------------
/// This connection class creates a SQLite environment with the data in
/// the provided datablock as tables.
class connection final
{
public:
/// Constructor
connection(datablock &db);
/// Destructor
~connection();
friend class transaction;
/// \brief Return true if the string @a sql contains a complete statement.
[[nodiscard]] bool is_complete_statement(const std::string &sql) const;
/// \brief Execute the sql in @a query returning an iterable result
result exec(std::string query);
/// \brief Execute the sql in @a query returning an iterable result.
/// Updates @a tail with what remains after the first statement in @a query
result exec(std::string query, std::string &tail);
/// \brief Return true if the underlying data was modified by any query.
[[nodiscard]] bool is_modified() const;
private:
struct connection_impl *m_impl;
};
} // namespace cif::cql