#![allow(unsafe_code)]
mod api;
mod runtime;
mod sys;
use std::{
cell::{Cell, RefCell},
collections::{BTreeMap, HashMap},
ffi::{CStr, CString},
fmt,
hash::BuildHasher,
marker::PhantomData,
os::raw::c_char,
ptr::NonNull,
rc::Rc,
};
use serde_json::Value as JsonValue;
use sys as ffi;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub struct Error {
pub code: i32,
pub message: String,
}
impl Error {
fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.message.is_empty() {
write!(f, "velr error (code {})", self.code)
} else {
write!(f, "velr error (code {}): {}", self.code, self.message)
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, PartialEq)]
pub enum QueryValue {
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
List(Vec<QueryValue>),
Map(BTreeMap<String, QueryValue>),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct QueryParams {
values: BTreeMap<String, QueryValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryParamError {
message: String,
}
impl QueryParamError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for QueryParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for QueryParamError {}
impl From<QueryParamError> for Error {
fn from(value: QueryParamError) -> Self {
Error::new(ffi::velr_code::VELR_EARG as i32, value.to_string())
}
}
pub trait TryIntoQueryValue {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError>;
}
impl QueryParams {
pub fn new() -> Self {
Self::default()
}
pub fn with<V>(
mut self,
name: impl Into<String>,
value: V,
) -> std::result::Result<Self, QueryParamError>
where
V: TryIntoQueryValue,
{
self.insert(name, value)?;
Ok(self)
}
pub fn insert<V>(
&mut self,
name: impl Into<String>,
value: V,
) -> std::result::Result<(), QueryParamError>
where
V: TryIntoQueryValue,
{
let name = name.into();
if name.is_empty() {
return Err(QueryParamError::new("query parameter name cannot be empty"));
}
if name.starts_with('$') {
return Err(QueryParamError::new(
"query parameter name should not include the leading `$`",
));
}
let value = value.try_into_query_value()?;
validate_query_value(&value)?;
self.values.insert(name, value);
Ok(())
}
pub fn get(&self, name: &str) -> Option<&QueryValue> {
self.values.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &QueryValue)> {
self.values
.iter()
.map(|(name, value)| (name.as_str(), value))
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn len(&self) -> usize {
self.values.len()
}
}
#[macro_export]
macro_rules! params {
() => {
::std::result::Result::<$crate::QueryParams, $crate::QueryParamError>::Ok(
$crate::QueryParams::new(),
)
};
(@insert $params:ident,) => {};
(@insert $params:ident) => {};
(@insert $params:ident, $name:ident : $value:expr, $($rest:tt)*) => {
$params.insert(::std::stringify!($name), $value)?;
$crate::params!(@insert $params, $($rest)*);
};
(@insert $params:ident, $name:ident : $value:expr) => {
$params.insert(::std::stringify!($name), $value)?;
};
(@insert $params:ident, $name:literal => $value:expr, $($rest:tt)*) => {
$params.insert($name, $value)?;
$crate::params!(@insert $params, $($rest)*);
};
(@insert $params:ident, $name:literal => $value:expr) => {
$params.insert($name, $value)?;
};
($($tt:tt)+) => {{
let mut params = $crate::QueryParams::new();
let result: ::std::result::Result<$crate::QueryParams, $crate::QueryParamError> = (|| {
$crate::params!(@insert params, $($tt)+);
::std::result::Result::Ok(params)
})();
result
}};
}
impl TryIntoQueryValue for QueryValue {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
validate_query_value(&self)?;
Ok(self)
}
}
impl TryIntoQueryValue for () {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::Null)
}
}
impl<T> TryIntoQueryValue for Option<T>
where
T: TryIntoQueryValue,
{
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
match self {
Some(value) => value.try_into_query_value(),
None => Ok(QueryValue::Null),
}
}
}
impl TryIntoQueryValue for bool {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::Bool(self))
}
}
impl TryIntoQueryValue for i64 {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::Integer(self))
}
}
macro_rules! signed_int_value {
($($ty:ty),* $(,)?) => {
$(
impl TryIntoQueryValue for $ty {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::Integer(i64::from(self)))
}
}
)*
};
}
signed_int_value!(i8, i16, i32);
impl TryIntoQueryValue for isize {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
let value = i64::try_from(self)
.map_err(|_| QueryParamError::new("isize parameter does not fit Cypher INTEGER"))?;
Ok(QueryValue::Integer(value))
}
}
macro_rules! unsigned_int_value {
($($ty:ty),* $(,)?) => {
$(
impl TryIntoQueryValue for $ty {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
let value = i64::try_from(self).map_err(|_| {
QueryParamError::new(concat!(
stringify!($ty),
" parameter does not fit Cypher INTEGER"
))
})?;
Ok(QueryValue::Integer(value))
}
}
)*
};
}
unsigned_int_value!(u8, u16, u32, u64, usize);
impl TryIntoQueryValue for f64 {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
if !self.is_finite() {
return Err(QueryParamError::new(
"floating point query parameters must be finite",
));
}
Ok(QueryValue::Float(self))
}
}
impl TryIntoQueryValue for f32 {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
if !self.is_finite() {
return Err(QueryParamError::new(
"floating point query parameters must be finite",
));
}
Ok(QueryValue::Float(f64::from(self)))
}
}
impl TryIntoQueryValue for String {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::String(self))
}
}
impl TryIntoQueryValue for &str {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
Ok(QueryValue::String(self.to_string()))
}
}
impl<T> TryIntoQueryValue for Vec<T>
where
T: TryIntoQueryValue,
{
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
let mut out = Vec::with_capacity(self.len());
for value in self {
let value = value.try_into_query_value()?;
validate_query_value(&value)?;
out.push(value);
}
Ok(QueryValue::List(out))
}
}
impl<T> TryIntoQueryValue for BTreeMap<String, T>
where
T: TryIntoQueryValue,
{
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
let mut out = BTreeMap::new();
for (key, value) in self {
let value = value.try_into_query_value()?;
validate_query_value(&value)?;
out.insert(key, value);
}
Ok(QueryValue::Map(out))
}
}
impl<T, S> TryIntoQueryValue for HashMap<String, T, S>
where
T: TryIntoQueryValue,
S: BuildHasher,
{
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
let mut out = BTreeMap::new();
for (key, value) in self {
let value = value.try_into_query_value()?;
validate_query_value(&value)?;
out.insert(key, value);
}
Ok(QueryValue::Map(out))
}
}
impl TryIntoQueryValue for JsonValue {
fn try_into_query_value(self) -> std::result::Result<QueryValue, QueryParamError> {
match self {
JsonValue::Null => Ok(QueryValue::Null),
JsonValue::Bool(value) => Ok(QueryValue::Bool(value)),
JsonValue::Number(value) => {
if let Some(value) = value.as_i64() {
Ok(QueryValue::Integer(value))
} else if let Some(value) = value.as_u64() {
let value = i64::try_from(value).map_err(|_| {
QueryParamError::new("JSON integer parameter does not fit Cypher INTEGER")
})?;
Ok(QueryValue::Integer(value))
} else if let Some(value) = value.as_f64() {
if !value.is_finite() {
return Err(QueryParamError::new(
"floating point query parameters must be finite",
));
}
Ok(QueryValue::Float(value))
} else {
Err(QueryParamError::new("unsupported JSON number parameter"))
}
}
JsonValue::String(value) => Ok(QueryValue::String(value)),
JsonValue::Array(values) => {
let mut out = Vec::with_capacity(values.len());
for value in values {
out.push(value.try_into_query_value()?);
}
Ok(QueryValue::List(out))
}
JsonValue::Object(values) => {
let mut out = BTreeMap::new();
for (key, value) in values {
out.insert(key, value.try_into_query_value()?);
}
Ok(QueryValue::Map(out))
}
}
}
}
fn validate_query_value(value: &QueryValue) -> std::result::Result<(), QueryParamError> {
match value {
QueryValue::Float(value) if !value.is_finite() => Err(QueryParamError::new(
"floating point query parameters must be finite",
)),
QueryValue::List(values) => {
for value in values {
validate_query_value(value)?;
}
Ok(())
}
QueryValue::Map(values) => {
for value in values.values() {
validate_query_value(value)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn query_value_to_json(value: &QueryValue) -> JsonValue {
match value {
QueryValue::Null => JsonValue::Null,
QueryValue::Bool(value) => JsonValue::Bool(*value),
QueryValue::Integer(value) => JsonValue::Number(serde_json::Number::from(*value)),
QueryValue::Float(value) => JsonValue::Number(
serde_json::Number::from_f64(*value)
.expect("QueryValue::Float is validated to be finite"),
),
QueryValue::String(value) => JsonValue::String(value.clone()),
QueryValue::List(values) => {
JsonValue::Array(values.iter().map(query_value_to_json).collect())
}
QueryValue::Map(values) => JsonValue::Object(
values
.iter()
.map(|(key, value)| (key.clone(), query_value_to_json(value)))
.collect(),
),
}
}
fn missing_runtime_symbol(name: &str) -> Error {
Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("loaded Velr runtime does not expose {name}"),
)
}
fn require_runtime_symbol<T: Copy>(symbol: Option<T>, name: &str) -> Result<T> {
symbol.ok_or_else(|| missing_runtime_symbol(name))
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct QueryOptions {
pub max_result_rows: Option<usize>,
pub params: QueryParams,
}
impl QueryOptions {
pub fn new() -> Self {
Self::default()
}
pub fn max_result_rows(max_result_rows: usize) -> Self {
Self {
max_result_rows: Some(max_result_rows),
params: QueryParams::new(),
}
}
pub fn with_max_result_rows(mut self, max_result_rows: usize) -> Self {
self.max_result_rows = Some(max_result_rows);
self
}
pub fn with_params(mut self, params: QueryParams) -> Self {
self.params = params;
self
}
pub fn with_param<V>(
mut self,
name: impl Into<String>,
value: V,
) -> std::result::Result<Self, QueryParamError>
where
V: TryIntoQueryValue,
{
self.params.insert(name, value)?;
Ok(self)
}
}
struct RawQueryParams {
ptr: NonNull<ffi::velr_query_params>,
free: unsafe extern "C" fn(*mut ffi::velr_query_params),
}
impl RawQueryParams {
fn from_query_params(params: &QueryParams) -> Result<Option<Self>> {
if params.is_empty() {
return Ok(None);
}
let a = velr_api()?;
let ptr = unsafe { (a.velr_query_params_new)() };
let ptr = NonNull::new(ptr).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_query_params_new returned null",
)
})?;
let out = Self {
ptr,
free: a.velr_query_params_free,
};
for (name, value) in params.iter() {
out.set(name, value)?;
}
Ok(Some(out))
}
fn set(&self, name: &str, value: &QueryValue) -> Result<()> {
let a = velr_api()?;
let name = raw_strview(name.as_bytes());
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
match value {
QueryValue::Null => {
(a.velr_query_params_set_null)(self.ptr.as_ptr(), name, &mut err)
}
QueryValue::Bool(value) => (a.velr_query_params_set_bool)(
self.ptr.as_ptr(),
name,
i32::from(*value),
&mut err,
),
QueryValue::Integer(value) => {
(a.velr_query_params_set_i64)(self.ptr.as_ptr(), name, *value, &mut err)
}
QueryValue::Float(value) => {
(a.velr_query_params_set_f64)(self.ptr.as_ptr(), name, *value, &mut err)
}
QueryValue::String(value) => (a.velr_query_params_set_text)(
self.ptr.as_ptr(),
name,
raw_strview(value.as_bytes()),
&mut err,
),
QueryValue::List(_) | QueryValue::Map(_) => {
let json = serde_json::to_vec(&query_value_to_json(value)).map_err(|e| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("failed to encode query parameter JSON: {e}"),
)
})?;
(a.velr_query_params_set_json)(
self.ptr.as_ptr(),
name,
raw_strview(&json),
&mut err,
)
}
}
};
rc_to_result(rc, err)
}
}
impl Drop for RawQueryParams {
fn drop(&mut self) {
unsafe {
(self.free)(self.ptr.as_ptr());
}
}
}
fn raw_strview(bytes: &[u8]) -> ffi::velr_strview {
ffi::velr_strview {
ptr: bytes.as_ptr(),
len: bytes.len(),
}
}
fn raw_query_options(
options: &QueryOptions,
) -> Result<(ffi::velr_query_options, Option<RawQueryParams>)> {
let params = RawQueryParams::from_query_params(&options.params)?;
let raw = ffi::velr_query_options {
has_max_result_rows: i32::from(options.max_result_rows.is_some()),
max_result_rows: options.max_result_rows.unwrap_or(0),
params: params
.as_ref()
.map(|params| params.ptr.as_ptr() as *const ffi::velr_query_params)
.unwrap_or(std::ptr::null()),
};
Ok((raw, params))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrationStatus {
AlreadyCurrent,
Migrated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationReport {
pub from_version: i32,
pub to_version: i32,
pub status: MigrationStatus,
pub steps: Vec<String>,
}
fn velr_api() -> Result<&'static api::Api> {
Ok(&runtime::runtime()?.api)
}
unsafe fn take_err(p: *mut c_char) -> String {
if p.is_null() {
return String::new();
}
let s = CStr::from_ptr(p).to_string_lossy().into_owned();
if let Ok(a) = velr_api() {
(a.velr_string_free)(p);
}
s
}
fn free_unexpected_err(err: *mut c_char) {
if !err.is_null() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_string_free)(err) };
}
}
}
fn take_owned_bytes(a: &api::Api, ptr: *mut u8, len: usize, what: &str) -> Result<Vec<u8>> {
if ptr.is_null() {
return if len == 0 {
Ok(Vec::new())
} else {
Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("{what} returned null pointer with non-zero length"),
))
};
}
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec();
unsafe { (a.velr_free)(ptr, len) };
Ok(bytes)
}
fn rc_to_result(rc: ffi::velr_code, err: *mut c_char) -> Result<()> {
let code = rc as i32;
if code == ffi::velr_code::VELR_OK as i32 {
free_unexpected_err(err);
Ok(())
} else {
let msg = unsafe { take_err(err) };
Err(Error::new(code, msg))
}
}
fn rc_to_result_noerr(rc: ffi::velr_code, context: impl Into<String>) -> Result<()> {
let code = rc as i32;
if code == ffi::velr_code::VELR_OK as i32 {
Ok(())
} else {
Err(Error::new(code, context.into()))
}
}
fn strview_to_string(v: ffi::velr_strview, what: &str) -> Result<String> {
if v.len == 0 {
return Ok(String::new());
}
if v.ptr.is_null() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("{what} is null with non-zero length"),
));
}
let bytes = unsafe { std::slice::from_raw_parts(v.ptr, v.len) };
let s = std::str::from_utf8(bytes).map_err(|_| {
Error::new(
ffi::velr_code::VELR_EUTF as i32,
format!("{what} is not valid UTF-8"),
)
})?;
Ok(s.to_string())
}
fn opt_strview_to_string(v: ffi::velr_strview, what: &str) -> Result<Option<String>> {
if v.ptr.is_null() && v.len == 0 {
return Ok(None);
}
Ok(Some(strview_to_string(v, what)?))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainPlanMeta {
pub plan_id: String,
pub cypher: String,
pub step_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainStepMeta {
pub step_no: usize,
pub group_id: String,
pub op_index: String,
pub phase: String,
pub title: String,
pub source: String,
pub note: Option<String>,
pub statement_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainStatementMeta {
pub stmt_id: String,
pub kind: String,
pub sql: String,
pub note: Option<String>,
pub sqlite_plan_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainStatement {
pub meta: ExplainStatementMeta,
pub sqlite_plan: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainStep {
pub meta: ExplainStepMeta,
pub statements: Vec<ExplainStatement>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainPlan {
pub meta: ExplainPlanMeta,
pub steps: Vec<ExplainStep>,
}
pub struct ExplainTrace {
trace: NonNull<ffi::velr_explain_trace>,
_nosend: PhantomData<Rc<()>>, }
impl ExplainTrace {
fn from_raw(ptr: *mut ffi::velr_explain_trace) -> Result<Self> {
let trace = NonNull::new(ptr).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"runtime returned null explain trace",
)
})?;
Ok(Self {
trace,
_nosend: PhantomData,
})
}
pub fn plan_count(&self) -> Result<usize> {
let a = velr_api()?;
Ok(unsafe { (a.velr_explain_trace_plan_count)(self.trace.as_ptr()) })
}
pub fn plan_meta(&self, plan_idx: usize) -> Result<ExplainPlanMeta> {
let a = velr_api()?;
let mut out = std::mem::MaybeUninit::<ffi::velr_explain_plan_meta>::uninit();
let rc = unsafe {
(a.velr_explain_trace_plan_meta)(self.trace.as_ptr(), plan_idx, out.as_mut_ptr())
};
rc_to_result_noerr(
rc,
format!("velr_explain_trace_plan_meta failed at plan_idx={plan_idx}"),
)?;
let out = unsafe { out.assume_init() };
Ok(ExplainPlanMeta {
plan_id: strview_to_string(out.plan_id, "plan_id")?,
cypher: strview_to_string(out.cypher, "cypher")?,
step_count: out.step_count,
})
}
pub fn step_count(&self, plan_idx: usize) -> Result<usize> {
Ok(self.plan_meta(plan_idx)?.step_count)
}
pub fn step_meta(&self, plan_idx: usize, step_idx: usize) -> Result<ExplainStepMeta> {
let a = velr_api()?;
let mut out = std::mem::MaybeUninit::<ffi::velr_explain_step_meta>::uninit();
let rc = unsafe {
(a.velr_explain_trace_step_meta)(
self.trace.as_ptr(),
plan_idx,
step_idx,
out.as_mut_ptr(),
)
};
rc_to_result_noerr(
rc,
format!(
"velr_explain_trace_step_meta failed at plan_idx={plan_idx}, step_idx={step_idx}"
),
)?;
let out = unsafe { out.assume_init() };
Ok(ExplainStepMeta {
step_no: out.step_no,
group_id: strview_to_string(out.group_id, "group_id")?,
op_index: strview_to_string(out.op_index, "op_index")?,
phase: strview_to_string(out.phase, "phase")?,
title: strview_to_string(out.title, "title")?,
source: strview_to_string(out.source, "source")?,
note: opt_strview_to_string(out.note, "step.note")?,
statement_count: out.statement_count,
})
}
pub fn statement_count(&self, plan_idx: usize, step_idx: usize) -> Result<usize> {
Ok(self.step_meta(plan_idx, step_idx)?.statement_count)
}
pub fn statement_meta(
&self,
plan_idx: usize,
step_idx: usize,
stmt_idx: usize,
) -> Result<ExplainStatementMeta> {
let a = velr_api()?;
let mut out = std::mem::MaybeUninit::<ffi::velr_explain_stmt_meta>::uninit();
let rc = unsafe {
(a.velr_explain_trace_statement_meta)(
self.trace.as_ptr(),
plan_idx,
step_idx,
stmt_idx,
out.as_mut_ptr(),
)
};
rc_to_result_noerr(
rc,
format!(
"velr_explain_trace_statement_meta failed at plan_idx={plan_idx}, step_idx={step_idx}, stmt_idx={stmt_idx}"
),
)?;
let out = unsafe { out.assume_init() };
Ok(ExplainStatementMeta {
stmt_id: strview_to_string(out.stmt_id, "stmt_id")?,
kind: strview_to_string(out.kind, "kind")?,
sql: strview_to_string(out.sql, "sql")?,
note: opt_strview_to_string(out.note, "statement.note")?,
sqlite_plan_count: out.sqlite_plan_count,
})
}
pub fn sqlite_plan_count(
&self,
plan_idx: usize,
step_idx: usize,
stmt_idx: usize,
) -> Result<usize> {
Ok(self
.statement_meta(plan_idx, step_idx, stmt_idx)?
.sqlite_plan_count)
}
pub fn sqlite_plan_detail(
&self,
plan_idx: usize,
step_idx: usize,
stmt_idx: usize,
detail_idx: usize,
) -> Result<String> {
let a = velr_api()?;
let mut out = std::mem::MaybeUninit::<ffi::velr_strview>::uninit();
let rc = unsafe {
(a.velr_explain_trace_sqlite_plan_detail)(
self.trace.as_ptr(),
plan_idx,
step_idx,
stmt_idx,
detail_idx,
out.as_mut_ptr(),
)
};
rc_to_result_noerr(
rc,
format!(
"velr_explain_trace_sqlite_plan_detail failed at plan_idx={plan_idx}, step_idx={step_idx}, stmt_idx={stmt_idx}, detail_idx={detail_idx}"
),
)?;
let out = unsafe { out.assume_init() };
strview_to_string(out, "sqlite_plan_detail")
}
pub fn sqlite_plan_details(
&self,
plan_idx: usize,
step_idx: usize,
stmt_idx: usize,
) -> Result<Vec<String>> {
let n = self.sqlite_plan_count(plan_idx, step_idx, stmt_idx)?;
let mut out = Vec::with_capacity(n);
for i in 0..n {
out.push(self.sqlite_plan_detail(plan_idx, step_idx, stmt_idx, i)?);
}
Ok(out)
}
pub fn snapshot(&self) -> Result<Vec<ExplainPlan>> {
let plan_count = self.plan_count()?;
let mut plans = Vec::with_capacity(plan_count);
for plan_idx in 0..plan_count {
let plan_meta = self.plan_meta(plan_idx)?;
let mut steps = Vec::with_capacity(plan_meta.step_count);
for step_idx in 0..plan_meta.step_count {
let step_meta = self.step_meta(plan_idx, step_idx)?;
let mut statements = Vec::with_capacity(step_meta.statement_count);
for stmt_idx in 0..step_meta.statement_count {
let stmt_meta = self.statement_meta(plan_idx, step_idx, stmt_idx)?;
let sqlite_plan = self.sqlite_plan_details(plan_idx, step_idx, stmt_idx)?;
statements.push(ExplainStatement {
meta: stmt_meta,
sqlite_plan,
});
}
steps.push(ExplainStep {
meta: step_meta,
statements,
});
}
plans.push(ExplainPlan {
meta: plan_meta,
steps,
});
}
Ok(plans)
}
pub fn compact_len(&self) -> Result<usize> {
let a = velr_api()?;
let mut len: usize = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc =
unsafe { (a.velr_explain_trace_compact_len)(self.trace.as_ptr(), &mut len, &mut err) };
rc_to_result(rc, err)?;
Ok(len)
}
pub fn to_compact_bytes(&self) -> Result<Vec<u8>> {
let a = velr_api()?;
let mut ptr: *mut u8 = std::ptr::null_mut();
let mut len: usize = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_explain_trace_compact_malloc)(self.trace.as_ptr(), &mut ptr, &mut len, &mut err)
};
rc_to_result(rc, err)?;
take_owned_bytes(a, ptr, len, "velr_explain_trace_compact_malloc")
}
pub fn to_compact_string(&self) -> Result<String> {
let bytes = self.to_compact_bytes()?;
let s = String::from_utf8(bytes).map_err(|e| {
Error::new(
ffi::velr_code::VELR_EUTF as i32,
format!("compact explain is not valid UTF-8: {e}"),
)
})?;
Ok(s)
}
pub fn write_compact(&self, mut out: impl std::io::Write) -> Result<()> {
let bytes = self.to_compact_bytes()?;
out.write_all(&bytes).map_err(|e| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("failed to write compact explain: {e}"),
)
})
}
}
impl Drop for ExplainTrace {
fn drop(&mut self) {
if let Ok(a) = velr_api() {
unsafe { (a.velr_explain_trace_close)(self.trace.as_ptr()) };
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum CellRef<'a> {
Null,
Bool(bool),
Integer(i64),
Float(f64),
Text(&'a [u8]),
Json(&'a [u8]),
}
impl<'a> CellRef<'a> {
pub fn as_str_utf8(&self) -> Option<std::result::Result<&'a str, std::str::Utf8Error>> {
match self {
CellRef::Text(b) => Some(std::str::from_utf8(b)),
_ => None,
}
}
}
pub struct Velr {
db: NonNull<ffi::velr_db>,
_not_sync: PhantomData<Cell<()>>, }
impl Velr {
pub fn open(path: Option<&str>) -> Result<Self> {
let a = velr_api()?;
let mut out_db: *mut ffi::velr_db = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let cpath;
let path_ptr = match path {
None => std::ptr::null(),
Some(p) => {
cpath = CString::new(p).map_err(|_| {
Error::new(ffi::velr_code::VELR_EUTF as i32, "path contains NUL")
})?;
cpath.as_ptr()
}
};
let rc = unsafe { (a.velr_open)(path_ptr, &mut out_db, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_db).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_open returned null db",
)
})?;
Ok(Self {
db: nn,
_not_sync: PhantomData,
})
}
pub fn open_readonly(path: &str) -> Result<Self> {
let a = velr_api()?;
let open_readonly = a.velr_open_existing_readonly.ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"loaded Velr runtime does not expose velr_open_existing_readonly",
)
})?;
let mut out_db: *mut ffi::velr_db = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let cpath = CString::new(path)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "path contains NUL"))?;
let rc = unsafe { open_readonly(cpath.as_ptr(), &mut out_db, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_db).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_open_existing_readonly returned null db",
)
})?;
Ok(Self {
db: nn,
_not_sync: PhantomData,
})
}
pub fn schema_version(&self) -> Result<i32> {
let a = velr_api()?;
let schema_version = require_runtime_symbol(a.velr_schema_version, "velr_schema_version")?;
let mut out = 0i32;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { schema_version(self.db.as_ptr(), &mut out, &mut err) };
rc_to_result(rc, err)?;
Ok(out)
}
pub fn current_schema_version(&self) -> Result<i32> {
let a = velr_api()?;
let current_schema_version =
require_runtime_symbol(a.velr_current_schema_version, "velr_current_schema_version")?;
let mut out = 0i32;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { current_schema_version(self.db.as_ptr(), &mut out, &mut err) };
rc_to_result(rc, err)?;
Ok(out)
}
pub fn needs_migration(&self) -> Result<bool> {
let a = velr_api()?;
let needs_migration =
require_runtime_symbol(a.velr_needs_migration, "velr_needs_migration")?;
let mut out = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { needs_migration(self.db.as_ptr(), &mut out, &mut err) };
rc_to_result(rc, err)?;
Ok(out != 0)
}
pub fn migrate(&self) -> Result<MigrationReport> {
let a = velr_api()?;
let migrate = require_runtime_symbol(a.velr_migrate, "velr_migrate")?;
let mut raw = ffi::velr_migration_report {
from_version: 0,
to_version: 0,
status: ffi::velr_migration_status::VELR_MIGRATION_ALREADY_CURRENT,
step_count: 0,
steps: std::ptr::null_mut(),
};
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { migrate(self.db.as_ptr(), &mut raw, &mut err) };
let result = rc_to_result(rc, err).and_then(|()| {
let status = match raw.status {
ffi::velr_migration_status::VELR_MIGRATION_ALREADY_CURRENT => {
MigrationStatus::AlreadyCurrent
}
ffi::velr_migration_status::VELR_MIGRATION_MIGRATED => MigrationStatus::Migrated,
};
let steps = if raw.steps.is_null() || raw.step_count == 0 {
Vec::new()
} else {
let detail = unsafe { CStr::from_ptr(raw.steps) }
.to_string_lossy()
.into_owned();
detail
.split(',')
.filter(|step| !step.is_empty())
.map(str::to_string)
.collect()
};
Ok(MigrationReport {
from_version: raw.from_version,
to_version: raw.to_version,
status,
steps,
})
});
if !raw.steps.is_null() {
if let Some(clear) = a.velr_migration_report_clear {
unsafe { clear(&mut raw) };
} else {
unsafe { (a.velr_string_free)(raw.steps) };
}
}
result
}
pub fn exec<'db>(&'db self, cypher: &str) -> Result<ExecTables<'db>> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_stream: *mut ffi::velr_stream = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_exec_start)(self.db.as_ptr(), cy.as_ptr(), &mut out_stream, &mut err)
};
rc_to_result(rc, err)?;
let nn = NonNull::new(out_stream).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_exec_start returned null stream",
)
})?;
Ok(ExecTables {
stream: Some(nn),
_db: PhantomData,
_nosend: PhantomData,
})
}
pub fn exec_with_options<'db>(
&'db self,
cypher: &str,
options: QueryOptions,
) -> Result<ExecTables<'db>> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let (raw_options, _raw_params) = raw_query_options(&options)?;
let mut out_stream: *mut ffi::velr_stream = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_exec_start_with_options)(
self.db.as_ptr(),
cy.as_ptr(),
&raw_options,
&mut out_stream,
&mut err,
)
};
rc_to_result(rc, err)?;
let nn = NonNull::new(out_stream).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_exec_start_with_options returned null stream",
)
})?;
Ok(ExecTables {
stream: Some(nn),
_db: PhantomData,
_nosend: PhantomData,
})
}
pub fn exec_one(&self, cypher: &str) -> Result<TableResult> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_table: *mut ffi::velr_table = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc =
unsafe { (a.velr_exec_one)(self.db.as_ptr(), cy.as_ptr(), &mut out_table, &mut err) };
rc_to_result(rc, err)?;
TableResult::from_raw(out_table)
}
pub fn exec_one_with_options(
&self,
cypher: &str,
options: QueryOptions,
) -> Result<TableResult> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let (raw_options, _raw_params) = raw_query_options(&options)?;
let mut out_table: *mut ffi::velr_table = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_exec_one_with_options)(
self.db.as_ptr(),
cy.as_ptr(),
&raw_options,
&mut out_table,
&mut err,
)
};
rc_to_result(rc, err)?;
TableResult::from_raw(out_table)
}
pub fn run(&self, cypher: &str) -> Result<()> {
let mut st = self.exec(cypher)?;
while let Some(mut t) = st.next_table()? {
t.for_each_row(|_| Ok(()))?;
}
Ok(())
}
pub fn run_with_options(&self, cypher: &str, options: QueryOptions) -> Result<()> {
let mut st = self.exec_with_options(cypher, options)?;
while let Some(mut t) = st.next_table()? {
t.for_each_row(|_| Ok(()))?;
}
Ok(())
}
pub fn exec_with_params<'db>(
&'db self,
cypher: &str,
params: QueryParams,
) -> Result<ExecTables<'db>> {
self.exec_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn exec_one_with_params(&self, cypher: &str, params: QueryParams) -> Result<TableResult> {
self.exec_one_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn run_with_params(&self, cypher: &str, params: QueryParams) -> Result<()> {
self.run_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn explain(&self, cypher: &str) -> Result<ExplainTrace> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_trace: *mut ffi::velr_explain_trace = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc =
unsafe { (a.velr_explain)(self.db.as_ptr(), cy.as_ptr(), &mut out_trace, &mut err) };
rc_to_result(rc, err)?;
ExplainTrace::from_raw(out_trace)
}
pub fn explain_analyze(&self, cypher: &str) -> Result<ExplainTrace> {
let a = velr_api()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_trace: *mut ffi::velr_explain_trace = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_explain_analyze)(self.db.as_ptr(), cy.as_ptr(), &mut out_trace, &mut err)
};
rc_to_result(rc, err)?;
ExplainTrace::from_raw(out_trace)
}
pub fn begin_tx(&self) -> Result<VelrTx<'_>> {
let a = velr_api()?;
let mut out_tx: *mut ffi::velr_tx = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_tx_begin)(self.db.as_ptr(), &mut out_tx, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_tx).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"velr_tx_begin returned null tx",
)
})?;
Ok(VelrTx {
tx: Some(nn),
named_savepoints: RefCell::new(Vec::new()),
_db: PhantomData,
_nosend: PhantomData,
})
}
#[cfg(feature = "arrow-ipc")]
pub fn bind_arrow(
&self,
logical: &str,
col_names: Vec<String>,
arrays: Vec<Box<dyn arrow2::array::Array>>,
) -> Result<()> {
arrow_bind::bind_arrow_db(self.db.as_ptr(), logical, col_names, arrays)
}
#[cfg(feature = "arrow-ipc")]
pub fn bind_arrow_chunks(
&self,
logical: &str,
col_names: Vec<String>,
chunks_per_col: Vec<Vec<Box<dyn arrow2::array::Array>>>,
) -> Result<()> {
arrow_bind::bind_arrow_chunks_db(self.db.as_ptr(), logical, col_names, chunks_per_col)
}
}
impl Drop for Velr {
fn drop(&mut self) {
if let Ok(a) = velr_api() {
unsafe { (a.velr_close)(self.db.as_ptr()) };
}
}
}
pub struct ExecTables<'db> {
stream: Option<NonNull<ffi::velr_stream>>,
_db: PhantomData<&'db Velr>,
_nosend: PhantomData<Rc<()>>, }
impl<'db> ExecTables<'db> {
pub fn next_table(&mut self) -> Result<Option<TableResult>> {
let a = velr_api()?;
let Some(stream) = self.stream else {
return Ok(None);
};
let mut out_table: *mut ffi::velr_table = std::ptr::null_mut();
let mut has: i32 = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_stream_next_table)(stream.as_ptr(), &mut out_table, &mut has, &mut err)
};
rc_to_result(rc, err)?;
if has == 0 {
unsafe { (a.velr_exec_close)(stream.as_ptr()) };
self.stream = None;
return Ok(None);
}
TableResult::from_raw(out_table).map(Some)
}
}
impl Drop for ExecTables<'_> {
fn drop(&mut self) {
if let Some(st) = self.stream.take() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_exec_close)(st.as_ptr()) };
}
}
}
}
pub struct TableResult {
table: NonNull<ffi::velr_table>,
col_names: Vec<String>,
col_count: usize,
_nosend: PhantomData<Rc<()>>, }
impl TableResult {
fn from_raw(ptr: *mut ffi::velr_table) -> Result<Self> {
let a = velr_api()?;
let table = NonNull::new(ptr)
.ok_or_else(|| Error::new(ffi::velr_code::VELR_EERR as i32, "null table"))?;
let build = (|| -> Result<(Vec<String>, usize)> {
let col_count = unsafe { (a.velr_table_column_count)(table.as_ptr()) };
let mut names = Vec::with_capacity(col_count);
for i in 0..col_count {
let mut p: *const u8 = std::ptr::null();
let mut len: usize = 0;
let rc = unsafe { (a.velr_table_column_name)(table.as_ptr(), i, &mut p, &mut len) };
if rc as i32 != ffi::velr_code::VELR_OK as i32 {
return Err(Error::new(
rc as i32,
format!("velr_table_column_name failed at idx={i}"),
));
}
let bytes: &[u8] = if len == 0 {
&[]
} else if p.is_null() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
format!("column name at idx={i} is null with non-zero length"),
));
} else {
unsafe { std::slice::from_raw_parts(p, len) }
};
let s = std::str::from_utf8(bytes).map_err(|_| {
Error::new(
ffi::velr_code::VELR_EUTF as i32,
format!("column name at idx={i} is not valid UTF-8"),
)
})?;
names.push(s.to_string());
}
Ok((names, col_count))
})();
match build {
Ok((col_names, col_count)) => Ok(Self {
table,
col_names,
col_count,
_nosend: PhantomData,
}),
Err(e) => {
unsafe { (a.velr_table_close)(table.as_ptr()) };
Err(e)
}
}
}
pub fn column_names(&self) -> &[String] {
&self.col_names
}
pub fn column_count(&self) -> usize {
self.col_count
}
pub fn rows<'t>(&'t mut self) -> Result<RowIter<'t>> {
let a = velr_api()?;
let mut out_rows: *mut ffi::velr_rows = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_table_rows_open)(self.table.as_ptr(), &mut out_rows, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_rows).ok_or_else(|| {
Error::new(ffi::velr_code::VELR_EERR as i32, "rows_open returned null")
})?;
Ok(RowIter {
rows: Some(nn),
col_count: self.col_count,
buf: vec![
ffi::velr_cell {
ty: ffi::velr_cell_type::VELR_NULL,
i64_: 0,
f64_: 0.0,
ptr: std::ptr::null(),
len: 0,
};
self.col_count
],
_table: PhantomData,
_nosend: PhantomData,
})
}
pub fn for_each_row<F>(&mut self, mut on_row: F) -> Result<()>
where
F: for<'row> FnMut(&[CellRef<'row>]) -> Result<()>,
{
let mut it = self.rows()?;
while it.next(|cells| on_row(cells))? {}
Ok(())
}
pub fn collect<T, F>(&mut self, mut map: F) -> Result<Vec<T>>
where
F: for<'row> FnMut(&[CellRef<'row>]) -> Result<T>,
{
let mut out = Vec::new();
self.for_each_row(|cells| {
out.push(map(cells)?);
Ok(())
})?;
Ok(out)
}
#[cfg(feature = "arrow-ipc")]
pub fn to_arrow_ipc_file(&mut self) -> Result<Vec<u8>> {
let a = velr_api()?;
let mut ptr: *mut u8 = std::ptr::null_mut();
let mut len: usize = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_table_ipc_file_malloc)(self.table.as_ptr(), &mut ptr, &mut len, &mut err)
};
rc_to_result(rc, err)?;
take_owned_bytes(a, ptr, len, "velr_table_ipc_file_malloc")
}
}
impl Drop for TableResult {
fn drop(&mut self) {
if let Ok(a) = velr_api() {
unsafe { (a.velr_table_close)(self.table.as_ptr()) };
}
}
}
pub struct RowIter<'t> {
rows: Option<NonNull<ffi::velr_rows>>,
col_count: usize,
buf: Vec<ffi::velr_cell>,
_table: PhantomData<&'t mut TableResult>,
_nosend: PhantomData<Rc<()>>, }
impl<'t> RowIter<'t> {
pub fn next<F>(&mut self, on_row: F) -> Result<bool>
where
F: for<'row> FnOnce(&[CellRef<'row>]) -> Result<()>,
{
let a = velr_api()?;
let Some(rows) = self.rows else {
return Ok(false);
};
let mut written: usize = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_rows_next)(
rows.as_ptr(),
self.buf.as_mut_ptr(),
self.buf.len(),
&mut written,
&mut err,
)
};
if rc == 0 {
free_unexpected_err(err);
unsafe { (a.velr_rows_close)(rows.as_ptr()) };
self.rows = None;
return Ok(false);
}
if rc < 0 {
let msg = unsafe { take_err(err) };
return Err(Error::new(rc, msg));
}
free_unexpected_err(err);
if written > self.buf.len() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
format!(
"velr_rows_next reported {} cells, buffer holds {}",
written,
self.buf.len()
),
));
}
let mut scratch: Vec<CellRef<'_>> = Vec::with_capacity(written);
for c in self.buf.iter().take(written) {
let cell = match c.ty {
ffi::velr_cell_type::VELR_NULL => CellRef::Null,
ffi::velr_cell_type::VELR_BOOL => CellRef::Bool(c.i64_ != 0),
ffi::velr_cell_type::VELR_INT64 => CellRef::Integer(c.i64_),
ffi::velr_cell_type::VELR_DOUBLE => CellRef::Float(c.f64_),
ffi::velr_cell_type::VELR_TEXT => {
let b: &[u8] = if c.len == 0 {
&[]
} else if c.ptr.is_null() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"VELR_TEXT cell had null pointer with non-zero length",
));
} else {
unsafe { std::slice::from_raw_parts(c.ptr, c.len) }
};
CellRef::Text(b)
}
ffi::velr_cell_type::VELR_JSON => {
let b: &[u8] = if c.len == 0 {
&[]
} else if c.ptr.is_null() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"VELR_JSON cell had null pointer with non-zero length",
));
} else {
unsafe { std::slice::from_raw_parts(c.ptr, c.len) }
};
CellRef::Json(b)
}
};
scratch.push(cell);
}
on_row(&scratch)?;
Ok(true)
}
}
impl Drop for RowIter<'_> {
fn drop(&mut self) {
if let Some(r) = self.rows.take() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_rows_close)(r.as_ptr()) };
}
}
}
}
#[derive(Debug)]
struct NamedSavepoint {
name: String,
sp: NonNull<ffi::velr_sp>,
}
pub struct VelrTx<'db> {
tx: Option<NonNull<ffi::velr_tx>>,
named_savepoints: RefCell<Vec<NamedSavepoint>>,
_db: PhantomData<&'db Velr>,
_nosend: PhantomData<Rc<()>>, }
impl<'db> VelrTx<'db> {
fn ptr(&self) -> Result<NonNull<ffi::velr_tx>> {
self.tx
.ok_or_else(|| Error::new(ffi::velr_code::VELR_ESTATE as i32, "tx already consumed"))
}
fn find_named_index(&self, name: &str) -> Option<usize> {
self.named_savepoints
.borrow()
.iter()
.position(|sp| sp.name == name)
}
pub fn exec<'tx>(&'tx self, cypher: &str) -> Result<ExecTablesTx<'tx>> {
let a = velr_api()?;
let tx = self.ptr()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_stream: *mut ffi::velr_stream_tx = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc =
unsafe { (a.velr_tx_exec_start)(tx.as_ptr(), cy.as_ptr(), &mut out_stream, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_stream).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"tx_exec_start returned null stream",
)
})?;
Ok(ExecTablesTx {
stream: Some(nn),
_tx: PhantomData,
_nosend: PhantomData,
})
}
pub fn exec_with_options<'tx>(
&'tx self,
cypher: &str,
options: QueryOptions,
) -> Result<ExecTablesTx<'tx>> {
let a = velr_api()?;
let tx = self.ptr()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let (raw_options, _raw_params) = raw_query_options(&options)?;
let mut out_stream: *mut ffi::velr_stream_tx = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_tx_exec_start_with_options)(
tx.as_ptr(),
cy.as_ptr(),
&raw_options,
&mut out_stream,
&mut err,
)
};
rc_to_result(rc, err)?;
let nn = NonNull::new(out_stream).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"tx_exec_start_with_options returned null stream",
)
})?;
Ok(ExecTablesTx {
stream: Some(nn),
_tx: PhantomData,
_nosend: PhantomData,
})
}
pub fn exec_one(&self, cypher: &str) -> Result<TableResult> {
let mut st = self.exec(cypher)?;
let first = match st.next_table()? {
Some(t) => t,
None => {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"query produced no result tables",
))
}
};
if st.next_table()?.is_some() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"query produced multiple tables; use exec()",
));
}
Ok(first)
}
pub fn exec_one_with_options(
&self,
cypher: &str,
options: QueryOptions,
) -> Result<TableResult> {
let mut st = self.exec_with_options(cypher, options)?;
let first = match st.next_table()? {
Some(t) => t,
None => {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"query produced no result tables",
))
}
};
if st.next_table()?.is_some() {
return Err(Error::new(
ffi::velr_code::VELR_EERR as i32,
"query produced multiple tables; use exec()",
));
}
Ok(first)
}
pub fn run(&self, cypher: &str) -> Result<()> {
let mut st = self.exec(cypher)?;
while let Some(mut t) = st.next_table()? {
t.for_each_row(|_| Ok(()))?;
}
Ok(())
}
pub fn run_with_options(&self, cypher: &str, options: QueryOptions) -> Result<()> {
let mut st = self.exec_with_options(cypher, options)?;
while let Some(mut t) = st.next_table()? {
t.for_each_row(|_| Ok(()))?;
}
Ok(())
}
pub fn exec_with_params<'tx>(
&'tx self,
cypher: &str,
params: QueryParams,
) -> Result<ExecTablesTx<'tx>> {
self.exec_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn exec_one_with_params(&self, cypher: &str, params: QueryParams) -> Result<TableResult> {
self.exec_one_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn run_with_params(&self, cypher: &str, params: QueryParams) -> Result<()> {
self.run_with_options(cypher, QueryOptions::new().with_params(params))
}
pub fn explain(&self, cypher: &str) -> Result<ExplainTrace> {
let a = velr_api()?;
let tx = self.ptr()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_trace: *mut ffi::velr_explain_trace = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_tx_explain)(tx.as_ptr(), cy.as_ptr(), &mut out_trace, &mut err) };
rc_to_result(rc, err)?;
ExplainTrace::from_raw(out_trace)
}
pub fn explain_analyze(&self, cypher: &str) -> Result<ExplainTrace> {
let a = velr_api()?;
let tx = self.ptr()?;
let cy = CString::new(cypher)
.map_err(|_| Error::new(ffi::velr_code::VELR_EUTF as i32, "openCypher contains NUL"))?;
let mut out_trace: *mut ffi::velr_explain_trace = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_tx_explain_analyze)(tx.as_ptr(), cy.as_ptr(), &mut out_trace, &mut err)
};
rc_to_result(rc, err)?;
ExplainTrace::from_raw(out_trace)
}
pub fn commit(mut self) -> Result<()> {
let a = velr_api()?;
self.named_savepoints.get_mut().clear();
let tx = self
.tx
.take()
.ok_or_else(|| Error::new(ffi::velr_code::VELR_ESTATE as i32, "tx already consumed"))?;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_tx_commit)(tx.as_ptr(), &mut err) };
rc_to_result(rc, err)
}
pub fn rollback(mut self) -> Result<()> {
let a = velr_api()?;
self.named_savepoints.get_mut().clear();
let tx = self
.tx
.take()
.ok_or_else(|| Error::new(ffi::velr_code::VELR_ESTATE as i32, "tx already consumed"))?;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_tx_rollback)(tx.as_ptr(), &mut err) };
rc_to_result(rc, err)
}
fn create_named_savepoint_raw(&self, name: &str) -> Result<NonNull<ffi::velr_sp>> {
let a = velr_api()?;
let tx = self.ptr()?;
let cname = CString::new(name).map_err(|_| {
Error::new(
ffi::velr_code::VELR_EUTF as i32,
"savepoint name contains NUL",
)
})?;
let mut out_sp: *mut ffi::velr_sp = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_tx_savepoint_named)(tx.as_ptr(), cname.as_ptr(), &mut out_sp, &mut err)
};
rc_to_result(rc, err)?;
NonNull::new(out_sp).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_EERR as i32,
"savepoint_named returned null",
)
})
}
pub fn savepoint<'tx>(&'tx self) -> Result<VelrSavepoint<'tx>> {
let a = velr_api()?;
let tx = self.ptr()?;
let mut out_sp: *mut ffi::velr_sp = std::ptr::null_mut();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_tx_savepoint)(tx.as_ptr(), &mut out_sp, &mut err) };
rc_to_result(rc, err)?;
let nn = NonNull::new(out_sp).ok_or_else(|| {
Error::new(ffi::velr_code::VELR_EERR as i32, "savepoint returned null")
})?;
Ok(VelrSavepoint {
sp: Some(nn),
_tx: PhantomData,
_nosend: PhantomData,
})
}
pub fn savepoint_named(&self, name: &str) -> Result<()> {
if self.find_named_index(name).is_some() {
return Err(Error::new(
ffi::velr_code::VELR_ESTATE as i32,
format!("named savepoint {name:?} already exists"),
));
}
let sp = self.create_named_savepoint_raw(name)?;
self.named_savepoints.borrow_mut().push(NamedSavepoint {
name: name.to_string(),
sp,
});
Ok(())
}
pub fn rollback_to(&self, name: &str) -> Result<()> {
let idx = self.find_named_index(name).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_ESTATE as i32,
format!("no such active named savepoint {name:?}"),
)
})?;
let target_name = {
let named = self.named_savepoints.borrow();
named[idx].name.clone()
};
let target_sp = {
let named = self.named_savepoints.borrow();
named[idx].sp
};
let a = velr_api()?;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_sp_rollback)(target_sp.as_ptr(), &mut err) };
rc_to_result(rc, err)?;
{
let mut named = self.named_savepoints.borrow_mut();
named.truncate(idx);
}
let recreated = self.create_named_savepoint_raw(&target_name)?;
self.named_savepoints.borrow_mut().push(NamedSavepoint {
name: target_name,
sp: recreated,
});
Ok(())
}
pub fn release_savepoint(&self, name: &str) -> Result<()> {
let a = velr_api()?;
let mut named = self.named_savepoints.borrow_mut();
let last_idx = named.len().checked_sub(1).ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_ESTATE as i32,
"no active named savepoints",
)
})?;
if named[last_idx].name != name {
return Err(Error::new(
ffi::velr_code::VELR_ESTATE as i32,
format!(
"release_savepoint({name:?}) requires {name:?} to be the most recent active named savepoint"
),
));
}
let entry = named.pop().unwrap();
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_sp_release)(entry.sp.as_ptr(), &mut err) };
rc_to_result(rc, err)
}
#[cfg(feature = "arrow-ipc")]
pub fn bind_arrow(
&self,
logical: &str,
col_names: Vec<String>,
arrays: Vec<Box<dyn arrow2::array::Array>>,
) -> Result<()> {
let tx = self.ptr()?;
arrow_bind::bind_arrow_tx(tx.as_ptr(), logical, col_names, arrays)
}
#[cfg(feature = "arrow-ipc")]
pub fn bind_arrow_chunks(
&self,
logical: &str,
col_names: Vec<String>,
chunks_per_col: Vec<Vec<Box<dyn arrow2::array::Array>>>,
) -> Result<()> {
let tx = self.ptr()?;
arrow_bind::bind_arrow_chunks_tx(tx.as_ptr(), logical, col_names, chunks_per_col)
}
}
impl Drop for VelrTx<'_> {
fn drop(&mut self) {
self.named_savepoints.get_mut().clear();
if let Some(tx) = self.tx.take() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_tx_close)(tx.as_ptr()) };
}
}
}
}
pub struct ExecTablesTx<'tx> {
stream: Option<NonNull<ffi::velr_stream_tx>>,
_tx: PhantomData<&'tx VelrTx<'tx>>,
_nosend: PhantomData<Rc<()>>, }
impl ExecTablesTx<'_> {
pub fn next_table(&mut self) -> Result<Option<TableResult>> {
let a = velr_api()?;
let Some(stream) = self.stream else {
return Ok(None);
};
let mut out_table: *mut ffi::velr_table = std::ptr::null_mut();
let mut has: i32 = 0;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe {
(a.velr_stream_tx_next_table)(stream.as_ptr(), &mut out_table, &mut has, &mut err)
};
rc_to_result(rc, err)?;
if has == 0 {
unsafe { (a.velr_exec_tx_close)(stream.as_ptr()) };
self.stream = None;
return Ok(None);
}
TableResult::from_raw(out_table).map(Some)
}
}
impl Drop for ExecTablesTx<'_> {
fn drop(&mut self) {
if let Some(st) = self.stream.take() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_exec_tx_close)(st.as_ptr()) };
}
}
}
}
#[must_use = "savepoint guards are RAII; bind the returned value to a variable or explicitly call release()/rollback()"]
pub struct VelrSavepoint<'tx> {
sp: Option<NonNull<ffi::velr_sp>>,
_tx: PhantomData<&'tx VelrTx<'tx>>,
_nosend: PhantomData<Rc<()>>, }
impl VelrSavepoint<'_> {
pub fn release(mut self) -> Result<()> {
let a = velr_api()?;
let sp = self.sp.take().ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_ESTATE as i32,
"savepoint already consumed",
)
})?;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_sp_release)(sp.as_ptr(), &mut err) };
rc_to_result(rc, err)
}
pub fn rollback(mut self) -> Result<()> {
let a = velr_api()?;
let sp = self.sp.take().ok_or_else(|| {
Error::new(
ffi::velr_code::VELR_ESTATE as i32,
"savepoint already consumed",
)
})?;
let mut err: *mut c_char = std::ptr::null_mut();
let rc = unsafe { (a.velr_sp_rollback)(sp.as_ptr(), &mut err) };
rc_to_result(rc, err)
}
}
impl Drop for VelrSavepoint<'_> {
fn drop(&mut self) {
if let Some(sp) = self.sp.take() {
if let Ok(a) = velr_api() {
unsafe { (a.velr_sp_close)(sp.as_ptr()) };
}
}
}
}
#[cfg(feature = "arrow-ipc")]
mod arrow_bind {
use super::*;
use std::mem::ManuallyDrop;
use arrow2::{
array::Array,
datatypes::Field,
ffi::{export_array_to_c, export_field_to_c, ArrowArray, ArrowSchema},
};
fn cstring(s: &str, what: &str) -> Result<CString> {
CString::new(s).map_err(|_| {
Error::new(
ffi::velr_code::VELR_EUTF as i32,
format!("{what} contains NUL"),
)
})
}
pub fn bind_arrow_db(
db: *mut ffi::velr_db,
logical: &str,
col_names: Vec<String>,
arrays: Vec<Box<dyn Array>>,
) -> Result<()> {
let a = super::velr_api()?;
bind_arrow_common(
|logical_ptr, schemas_pp, arrays_pp, names_ptr, n, err| unsafe {
(a.velr_bind_arrow)(db, logical_ptr, schemas_pp, arrays_pp, names_ptr, n, err)
},
logical,
col_names,
arrays,
)
}
pub fn bind_arrow_tx(
tx: *mut ffi::velr_tx,
logical: &str,
col_names: Vec<String>,
arrays: Vec<Box<dyn Array>>,
) -> Result<()> {
let a = super::velr_api()?;
bind_arrow_common(
|logical_ptr, schemas_pp, arrays_pp, names_ptr, n, err| unsafe {
(a.velr_tx_bind_arrow)(tx, logical_ptr, schemas_pp, arrays_pp, names_ptr, n, err)
},
logical,
col_names,
arrays,
)
}
fn bind_arrow_common(
f: impl FnOnce(
*const c_char,
*const *const ArrowSchema,
*const *const ArrowArray,
*const ffi::velr_strview,
usize,
*mut *mut c_char,
) -> ffi::velr_code,
logical: &str,
col_names: Vec<String>,
arrays: Vec<Box<dyn Array>>,
) -> Result<()> {
if col_names.is_empty() {
return Err(Error::new(
ffi::velr_code::VELR_EARG as i32,
"bind_arrow: no columns",
));
}
if arrays.len() != col_names.len() {
return Err(Error::new(
ffi::velr_code::VELR_EARG as i32,
format!(
"bind_arrow: arrays len {} != col_names len {}",
arrays.len(),
col_names.len()
),
));
}
let logical_c = cstring(logical, "logical")?;
let mut schemas: Vec<ArrowSchema> = Vec::with_capacity(col_names.len());
let mut array_cs: Vec<ManuallyDrop<ArrowArray>> = Vec::with_capacity(col_names.len());
let mut schema_ptrs: Vec<*const ArrowSchema> = Vec::with_capacity(col_names.len());
let mut array_ptrs: Vec<*const ArrowArray> = Vec::with_capacity(col_names.len());
let mut name_views: Vec<ffi::velr_strview> = Vec::with_capacity(col_names.len());
for (name, arr) in col_names.iter().zip(arrays.into_iter()) {
let field = Field::new(name.clone(), arr.data_type().clone(), true);
let schema = export_field_to_c(&field);
schemas.push(schema);
let a = ManuallyDrop::new(export_array_to_c(arr));
array_cs.push(a);
}
for i in 0..col_names.len() {
schema_ptrs.push(&schemas[i] as *const ArrowSchema);
array_ptrs.push((&*array_cs[i]) as *const ArrowArray);
let b = col_names[i].as_bytes();
name_views.push(ffi::velr_strview {
ptr: b.as_ptr(),
len: b.len(),
});
}
let mut err: *mut c_char = std::ptr::null_mut();
let rc = f(
logical_c.as_ptr(),
schema_ptrs.as_ptr(),
array_ptrs.as_ptr(),
name_views.as_ptr(),
col_names.len(),
&mut err,
);
super::rc_to_result(rc, err)
}
pub fn bind_arrow_chunks_db(
db: *mut ffi::velr_db,
logical: &str,
col_names: Vec<String>,
chunks_per_col: Vec<Vec<Box<dyn Array>>>,
) -> Result<()> {
let a = super::velr_api()?;
bind_chunks_common(
|logical_ptr, cols_ptr, names_ptr, n, err| unsafe {
(a.velr_bind_arrow_chunks)(db, logical_ptr, cols_ptr, names_ptr, n, err)
},
logical,
col_names,
chunks_per_col,
)
}
pub fn bind_arrow_chunks_tx(
tx: *mut ffi::velr_tx,
logical: &str,
col_names: Vec<String>,
chunks_per_col: Vec<Vec<Box<dyn Array>>>,
) -> Result<()> {
let a = super::velr_api()?;
bind_chunks_common(
|logical_ptr, cols_ptr, names_ptr, n, err| unsafe {
(a.velr_tx_bind_arrow_chunks)(tx, logical_ptr, cols_ptr, names_ptr, n, err)
},
logical,
col_names,
chunks_per_col,
)
}
fn bind_chunks_common(
f: impl FnOnce(
*const c_char,
*const ffi::velr_arrow_chunks,
*const ffi::velr_strview,
usize,
*mut *mut c_char,
) -> ffi::velr_code,
logical: &str,
col_names: Vec<String>,
chunks_per_col: Vec<Vec<Box<dyn Array>>>,
) -> Result<()> {
if col_names.is_empty() {
return Err(Error::new(
ffi::velr_code::VELR_EARG as i32,
"bind_arrow_chunks: no columns",
));
}
if chunks_per_col.len() != col_names.len() {
return Err(Error::new(
ffi::velr_code::VELR_EARG as i32,
format!(
"bind_arrow_chunks: chunks_per_col {} != col_names {}",
chunks_per_col.len(),
col_names.len()
),
));
}
let logical_c = cstring(logical, "logical")?;
let mut all_schema_storage: Vec<Vec<ArrowSchema>> = Vec::with_capacity(col_names.len());
let mut all_array_storage: Vec<Vec<ManuallyDrop<ArrowArray>>> =
Vec::with_capacity(col_names.len());
let mut all_schema_ptrs: Vec<Vec<*const ArrowSchema>> = Vec::with_capacity(col_names.len());
let mut all_array_ptrs: Vec<Vec<*const ArrowArray>> = Vec::with_capacity(col_names.len());
for (ci, chunks) in chunks_per_col.into_iter().enumerate() {
if chunks.is_empty() {
return Err(Error::new(
ffi::velr_code::VELR_EARG as i32,
format!("bind_arrow_chunks: col {ci} has 0 chunks"),
));
}
let mut schemas: Vec<ArrowSchema> = Vec::with_capacity(chunks.len());
let mut arrays: Vec<ManuallyDrop<ArrowArray>> = Vec::with_capacity(chunks.len());
for arr in chunks.into_iter() {
let field = Field::new(col_names[ci].clone(), arr.data_type().clone(), true);
schemas.push(export_field_to_c(&field));
arrays.push(ManuallyDrop::new(export_array_to_c(arr)));
}
let mut sp: Vec<*const ArrowSchema> = Vec::with_capacity(schemas.len());
let mut ap: Vec<*const ArrowArray> = Vec::with_capacity(arrays.len());
for i in 0..schemas.len() {
sp.push(&schemas[i] as *const ArrowSchema);
ap.push((&*arrays[i]) as *const ArrowArray);
}
all_schema_storage.push(schemas);
all_array_storage.push(arrays);
all_schema_ptrs.push(sp);
all_array_ptrs.push(ap);
}
let mut cols_desc: Vec<ffi::velr_arrow_chunks> = Vec::with_capacity(col_names.len());
for i in 0..col_names.len() {
cols_desc.push(ffi::velr_arrow_chunks {
schemas: all_schema_ptrs[i].as_ptr(),
arrays: all_array_ptrs[i].as_ptr(),
chunk_count: all_schema_ptrs[i].len(),
});
}
let mut name_views: Vec<ffi::velr_strview> = Vec::with_capacity(col_names.len());
for name in &col_names {
let b = name.as_bytes();
name_views.push(ffi::velr_strview {
ptr: b.as_ptr(),
len: b.len(),
});
}
let mut err: *mut c_char = std::ptr::null_mut();
let rc = f(
logical_c.as_ptr(),
cols_desc.as_ptr(),
name_views.as_ptr(),
col_names.len(),
&mut err,
);
super::rc_to_result(rc, err)
}
}