Programming in Rust

Getting Started

Programming YottaDB in the Rust language is accomplished through yottadb crate, which is a Rust wrapper around the C implementation of YottaDB.

The YottaDB Rust wrapper requires a minimum YottaDB version of r1.30 and is tested with a minimum Rust version of 1.40.

Install YottaDB

Follow the instructions in the Quick Start section to install YottaDB.

Install Rust

The next step is to install Rust. Follow the instructions provided here to get setup with Rust on your system.

Install YDBRust

Clone the YDBRust repository and run an example.

git clone https://gitlab.com/YottaDB/Lang/YDBRust/
cd YDBRust
echo 'yottadb = "2.0.0"' >> Cargo.toml
cargo run --example say_hello_rust

Rust API

There are two major APIs that are part of the Rust wrapper:

  • craw, the FFI bindings generated directly by bindgen. These are not recommended for normal use, but are available in case functionality is needed beyond that provided by the Context API.

  • The main Context API, which is a safe wrapper around the C API which keeps track of the current tptoken and an error buffer. The reason this metadata is necessary is because this crate binds to the threaded version of YottaDB, which requires a tptoken and err_buffer. See transaction processing for more details on tptoken and transactions.

  • Most operations are encapsulated in methods in the KeyContext struct. Iteration helpers are available to iterate over values in the database in a variety of ways.

Note

To run any of the examples below, create a file (e.g., rust_example.rs) under the examples/ sub-directory and run it, from the YDBRust directory, using the following command:

$ cargo run --quiet --example rust_example

Example:

A basic database operation (set a value, retrieve it, and delete it).

use yottadb::{Context, KeyContext, DeleteType, YDBResult};

fn main() -> YDBResult<()> {
    let ctx = Context::new();
    let key = KeyContext::new(&ctx, "^MyGlobal", &["SubscriptA", "42"]);

    key.set("This is a persistent message")?;
    let buffer = key.get()?;

    assert_eq!(&buffer, b"This is a persistent message");
    println!("{:?}", String::from_utf8(buffer).unwrap());
    key.delete(DeleteType::DelNode)?;
    Ok(())
}

Output:

"This is a persistent message"

Macros

ci_t

macro_rules! ci_t {
    ($tptoken: expr, $err_buffer: expr, $routine: expr $(, $args: expr)* $(,)?) => { ... };
}

ci_t macro is used to make an FFI call to M.

Each argument passed (after routine) must correspond to the appropriate argument expected by routine. If routine returns a value, the first argument must be a pointer to an out parameter in which to store the value. All arguments must be representable as C types.

Example:

Call the M routine described by HelloWorld1 in the call-in table. See also examples/m-ffi/helloworld1.m and examples/m-ffi/calltab.ci.

use std::ffi::CString;
use std::os::raw::c_char;
use yottadb::{craw, ci_t, TpToken};
use std::env;

fn main(){
     env::set_var("ydb_routines", "examples/m-ffi");
     env::set_var("ydb_ci", "examples/m-ffi/calltab.ci");

     let mut buf = Vec::<u8>::with_capacity(100);
     let mut msg = craw::ydb_string_t { length: buf.capacity() as u64, address: buf.as_mut_ptr() as *mut c_char };

     let routine = CString::new("HelloWorld1").unwrap();
     unsafe {
             ci_t!(TpToken::default(), Vec::new(), &routine, &mut msg as *mut _).unwrap();
             buf.set_len(msg.length as usize);
     }
     println!("{:?}", String::from_utf8_lossy(&buf));
     assert_eq!(&buf, b"macro call successful");
}

Output:

"macro call successful"

cip_t

macro_rules! cip_t {
    ($tptoken: expr, $err_buffer: expr, $routine: expr, $($args: expr),* $(,)?) => { ... };
}

cip_t macro is used to make a FFI call to M using a cached function descriptor.

Each argument passed (after routine) must correspond to the appropriate argument expected by routine. If routine returns a value, the first argument must be a pointer to an out parameter in which to store the value. All arguments must be representable as C types.

Example:

Call the M routine described by HelloWorld1 in the call-in table. See also examples/m-ffi/helloworld1.m and examples/m-ffi/calltab.ci.

use std::env;
use std::ffi::CString;
use std::os::raw::c_char;
use yottadb::{craw, cip_t, CallInDescriptor, TpToken};

fn main(){
     env::set_var("ydb_routines", "examples/m-ffi");
     env::set_var("ydb_ci", "examples/m-ffi/calltab.ci");

     let mut buf = Vec::<u8>::with_capacity(100);
     let mut msg = craw::ydb_string_t { length: buf.capacity() as u64, address: buf.as_mut_ptr() as *mut c_char };
     let mut routine = CallInDescriptor::new(CString::new("HelloWorld1").unwrap());
     unsafe {
             cip_t!(TpToken::default(), Vec::new(), &mut routine, &mut msg as *mut _).unwrap();
             buf.set_len(msg.length as usize);
     }
     println!("{:?}", String::from_utf8_lossy(&buf));
     assert_eq!(&buf, b"macro call successful");
 }

Output:

"macro call successful"

make_ckey

macro_rules! make_ckey {
    ( $ctx:expr, $var:expr $(,)?) => { ... };
    ( $ctx:expr, $gbl:expr $(, $x:expr)+ ) => { ... };
}

make_ckey macro is used to create a KeyContext with the given subscripts, provided a context.

Example:

use std::error::Error;
use yottadb::Context;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = yottadb::make_ckey!(ctx, "^hello", "world");
    println!("{:?}", key.data()?);
    Ok(())
}

Output:

ValueData

make_key

macro_rules! make_key {
    ( $var:expr $(,)? ) => { ... };
    ( $var: expr $( , $subscript: expr)+ $(,)? ) => { ... };
}

make_key macro provides a Key object for the given subscripts.

Example:

let my_key = make_key!("^MyTimeSeriesData", "5");

Struct yottadb::Context

A struct that keeps track of the current transaction and error buffer.

delete_excl()

As a wrapper for the C function ydb_delete_excl_st(), delete_excl() deletes all local variables except for those passed in saved_variables.

pub fn delete_excl(&self, saved_variables: &[&str]) -> YDBResult<()>

Passing an empty saved_variables slice deletes all local variables. Attempting to save a global or intrinsic variable is an error.

Example:

use yottadb::{Context, KeyContext, YDBResult, YDB_ERR_LVUNDEF};

fn main()-> YDBResult<()> {
    // Create three variables and set all
    let ctx = Context::new();
    let a = KeyContext::variable(&ctx, "deleteExclTestA");
    a.set("test data")?;
    let b = KeyContext::variable(&ctx, "deleteExclTestB");
    b.set("test data 2")?;
    let c = KeyContext::variable(&ctx, "deleteExclTestC");
    c.set("test data 3")?;

    println!("Before deleting any variables:");
    println!("a: {:?}", a.data()?);
    println!("b: {:?}", b.data()?);
    println!("c: {:?}", c.data()?);

    // Delete all variables except `a`
    ctx.delete_excl(&[&a.variable])?;
    assert_eq!(a.get()?, b"test data");
    assert_eq!(b.get().unwrap_err().status, YDB_ERR_LVUNDEF);
    assert_eq!(c.get().unwrap_err().status, YDB_ERR_LVUNDEF);
    println!("After deleting variables b and c:");
    println!("a: {:?}", a.data()?);
    println!("b: {:?}", b.data()?);
    println!("c :{:?}", c.data()?);

    // Delete `a` too
    ctx.delete_excl(&[])?;
    assert_eq!(a.get().unwrap_err().status, YDB_ERR_LVUNDEF);
    println!("After deleting variable a:");
    println!("a: {:?}", a.data()?);

    Ok(())
}

Output:

Before deleting any variables:
a: ValueData
b: ValueData
c: ValueData
After deleting variables b and c:
a: ValueData
b: NoData
c :NoData
After deleting variable a:
a: NoData

lock()

As a wrapper for the C function ydb_lock_st(), lock() acquires locks specified and releases all others.

pub fn lock(&self, timeout: Duration, locks: &[Key]) -> YDBResult<()>

This operation is atomic. If any lock cannot be acquired, all locks are released. The timeout specifies the maximum time to wait before returning an error. If no locks are specified, all locks are released.

Note that YottaDB locks are per-process, not per-thread.

For implementation reasons, there is a hard limit to the number of Keys that can be passed in locks:

  • 64-bit architecture: 10 Keys

  • 32-bit architecture: 9 Keys

If more than this number of keys are passed, YDB_ERR_MAXARGCNT will be returned.

Example:

use std::slice;
use std::time::Duration;
use yottadb::{Context, KeyContext, Key, TpToken};

// You can use either a `Key` or a `KeyContext` to acquire a lock.
// This uses a `KeyContext` to show that you need to use `.key` to get the inner `Key`.
let ctx = Context::new();
let a = KeyContext::variable(&ctx, "lockA");

// Release any locks held and acquire a single lock
// using `from_ref` here allows us to use `a` later without moving it
ctx.lock(Duration::from_secs(1), slice::from_ref(&a.key)).unwrap();

// Release any locks held and acquire multiple locks
let locks = vec![a.key, Key::variable("lockB")];
ctx.lock(Duration::from_secs(1), &locks).unwrap();

// Release all locks
ctx.lock(Duration::from_secs(0), &[]).unwrap();

str2zwr()

As a wrapper for the C function ydb_str2zwr_st(), str2zwr() serializes the given binary sequence to zwrite format, which is printable ASCII.

pub fn str2zwr(&self, original: &[u8]) -> YDBResult<Vec<u8>>

Example:

When ydb_chset=UTF-8 is set, this will preserve UTF-8 characters:

use yottadb::Context;

fn main() -> YDBResult<()>{
    let ctx = Context::new();
    let str2zwr = ctx.str2zwr(b"The quick brown dog\x08\x08\x08fox jumps over the lazy fox\x08\x08\x08dog.")?;
    println!("Original string: {}", "The quick brown dog\x08\x08\x08fox jumps over the lazy fox\x08\x08\x08dog.");
    println!("Zwrite formatted string: {:?}",String::from_utf8(str2zwr).unwrap());
    Ok(())
}

Output:

Original string: The quick brown fox jumps over the lazy dog.
Zwrite formatted string: "\"The quick brown dog\"_$C(8,8,8)_\"fox jumps over the lazy fox\"_$C(8,8,8)_\"dog.\""

When the input is invalid UTF-8, it will use the more verbose zwrite format:

use yottadb::Context;

fn main() -> YDBResult<()>{
    let ctx = Context::new();
    let str2zwr = ctx.str2zwr(b"\xff")?;
    assert!(std::str::from_utf8(b"\xff").is_err());
    assert_eq!(str2zwr, b"$ZCH(255)");
    println!("{:?}",String::from_utf8(str2zwr).unwrap());
    Ok(())
}

Output:

"$ZCH(255)"

tp()

pub fn tp<'a, F>(
    &'a self,
    f: F,
    trans_id: &str,
    locals_to_reset: &[&str]
) -> Result<(), Box<dyn Error + Send + Sync>>
where
    F: FnMut(&'a Self) -> Result<TransactionStatus, Box<dyn Error + Send + Sync>>,

As a wrapper for the C function ydb_tp_st(), tp() is used to execute a transaction, where f is the transaction to execute.

tp stands for “transaction processing”.

The parameter trans_id is the name logged for the transaction. If trans_id has the special value “BATCH”, durability is not enforced by YottaDB. See ydb_tp_st() for details.

The argument passed to f is a transaction processing token.

Application code can return a TransactionStatus in order to rollback or restart. tp() behaves as follows:

  • If f panics, the transaction is rolled back and the panic resumes afterwards.

  • If f returns Ok(TransactionStatus), the transaction will have the behavior documented under TransactionStatus (commit, restart, and rollback, respectively).

  • If f returns an Err(YDBError), the status from that error will be returned to the YottaDB engine. As a result, if the status for the YDBError is YDB_TP_RESTART, the transaction will be restarted. Otherwise, the transaction will be rolled back and the error returned from tp().

  • If f returns any other Err variant, the transaction will be rolled back and the error returned from tp().

f must be FnMut, not FnOnce, since the YottaDB engine may call f many times if necessary to ensure ACID properties. This may affect your application logic; within a transaction, the intrinsic variable $trestart has the number of times the transaction has been restarted.

Example:

Rollback a transaction if an operation fails:

use yottadb::{Context, KeyContext, TpToken, TransactionStatus};

let ctx = Context::new();
let var = KeyContext::variable(&ctx, "tpRollbackTest");
var.set("initial value")?;
println!("starting tp");
let maybe_err = ctx.tp(|ctx| {
    println!("in tp");
    fallible_operation()?;
    println!("succeeded");
    var.set("new value")?;
    Ok(TransactionStatus::Ok)
}, "BATCH", &[]);
let expected_val: &[_] = if maybe_err.is_ok() {
    b"new value"
} else {
    b"initial value"
};
assert_eq!(var.get()?, expected_val);

fn fallible_operation() -> Result<(), &'static str> {
    if rand::random() {
        Ok(())
    } else {
        Err("the operation failed")
    }
}

Retry a transaction until it succeeds:

use yottadb::{Context, TpToken, TransactionStatus};

let ctx = Context::new();
ctx.tp(|tptoken| {
    if fallible_operation().is_ok() {
        Ok(TransactionStatus::Ok)
    } else {
        Ok(TransactionStatus::Restart)
    }
}, "BATCH", &[]).unwrap();

fn fallible_operation() -> Result<(), ()> {
    if rand::random() {
        Ok(())
    } else {
        Err(())
    }
}

zwr2str()

As a wrapper for the C funtion ydb_zwr2str_st(), zwr2str() deserializes a zwrite formatted buffer to the original binary buffer.

pub fn zwr2str(&self, serialized: &[u8]) -> Result<Vec<u8>, YDBError>

Example:

use yottadb::{Context, YDBResult};

fn main() -> YDBResult<()>{
    let ctx = Context::new();

    // Use "$ZCH" (instead of "$C") below as that will work in both M and UTF-8 modes (of "ydb_chset" env var)
    // Note: Cannot use "$ZCHAR" below as "$ZCH" is the only input format recognized by "zwr2str()".
    let out_buf = ctx.zwr2str(b"\"The quick brown dog\"_$ZCH(8,8,8)_\"fox jumps over the lazy fox\"_$ZCH(8,8,8)_\"dog.\"")?;
    println!("Zwrite formatted string: {}","The quick brown dog\"_$ZCH(8,8,8)_\"fox jumps over the lazy fox\"_$ZCH(8,8,8)_\"dog.");
    println!("String after zwr2str: {}",String::from_utf8(out_buf).unwrap());

    Ok(())
}

Output:

Zwrite formatted string: The quick brown dog"_$ZCH(8,8,8)_"fox jumps over the lazy fox"_$ZCH(8,8,8)_"dog.
String after zwr2str: The quick brown fox jumps over the lazy dog.

zwr2str() writes directly to out_buf to avoid returning multiple output buffers.

Struct yottadb::KeyContext

A key which keeps track of the current transaction and error buffer.

Keys are used to get, set, and delete values in the database.

data()

As a wrapper for the C function ydb_data_st(), data() provides information about whether or not a global or local variable node has data and/or a subtree.

pub fn data(&self) -> YDBResult<DataReturn>

It returns the following information in DataReturn about a local or global variable node:

  • NoData: There is neither a value nor a subtree; i.e. it is undefined

  • ValueData: There is a value, but no subtree

  • TreeData: There is no value, but there is a subtree

  • ValueTreeData: There are both a value and a subtree

Example:

use yottadb::{Context, DataReturn, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = make_ckey!(ctx, "^helloDoesNotExist");

    assert_eq!(key.data()?, DataReturn::NoData);
    println!("{:?}", key.data()?);
    Ok(())
}

Output:

NoData

delete()

As a wrapper for the C function ydb_delete_st(), delete() deletes nodes in the local or global variable tree or subtree specified.

pub fn delete(&self, delete_type: DeleteType) -> YDBResult<()>

A value of DelNode or DelTree for DeleteType specifies whether to delete just the node at the root, leaving the (sub)tree intact, or to delete the node as well as the (sub)tree.

Example:

use yottadb::{Context, DataReturn, DeleteType, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = make_ckey!(ctx, "^helloDeleteMe");

    key.set("Hello world!")?;
    println!("{:?}", String::from_utf8(key.get()?).unwrap());
    key.delete(DeleteType::DelTree)?;

    assert_eq!(key.data()?, DataReturn::NoData);
    println!("{:?}", key.data()?);
    Ok(())
}

Output:

"Hello world!"
NoData

get()

As a wrapper for the C function ydb_get_st(), get() gets the value of this key from the database and returns the value.

pub fn get(&self) -> YDBResult<Vec<u8>>

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = make_ckey!(ctx, "^hello");

    key.set("Hello world!")?;
    let output_buffer = key.get()?;

    assert_eq!(output_buffer, b"Hello world!");
    println!( "{:?}", String::from_utf8(output_buffer).unwrap());
    Ok(())
}

Output:

"Hello world!"

get() can be used to get the value of an Intrinsic Variable as well.

Example:

use yottadb::{Context, KeyContext, YDBResult};

fn main() -> YDBResult<()> {
    let ctx = Context::new();
    let key = KeyContext::variable(&ctx, "$zyrelease");

    let zyrelease = key.get()?;

    assert_eq!(zyrelease, b"YottaDB r1.34 Linux x86_64");
    println!("$zyrelease: {}", String::from_utf8(zyrelease).unwrap());
    Ok(())
}

Output:

$zyrelease: YottaDB r1.34 Linux x86_64

increment()

As a wrapper for the C function ydb_incr_st(), increment() converts the value to a number and increments it based on the value specified by Option.

pub fn increment(&self, increment: Option<&[u8]>) -> YDBResult<Vec<u8>>

increment defaults to 1 if the value is None.

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = make_ckey!(ctx, "helloIncrementMe");

    key.set("0")?;
    let mut output_buffer = key.get()?;

    println!("Before increment: {:?}", String::from_utf8(output_buffer).unwrap());
    key.increment(None)?;
    output_buffer = key.get()?;

    assert_eq!(output_buffer, b"1");
    println!("Incremented by 1 (default): {:?}", String::from_utf8(output_buffer).unwrap());

    assert_eq!(key.increment(Some(b"100"))?, b"101");
    println!("Before increment : {:?}", "100" );
    output_buffer = key.get()?;

    assert_eq!(output_buffer, b"101");
    println!("After increment: {:?}", String::from_utf8(output_buffer).unwrap());
    Ok(())
}

Output:

Before increment: "0"
Incremented by 1 (default): "1"
Before increment : "100"
After increment: "101"

lock_decr()

As a wrapper for the C function ydb_lock_decr_st(), lock_decr() decrements the count of a lock held by the process.

When the count for a lock goes from 1 to 0, it is released. Attempting to decrement a lock not owned by the current process has no effect.

pub fn lock_decr(&self) -> YDBResult<()>

Example:

use yottadb::{Context, KeyContext};
use std::time::Duration;

let ctx = Context::new();
let key = KeyContext::variable(&ctx, "lockDecrTest");
key.lock_incr(Duration::from_secs(1))?;
key.lock_decr()?;

lock_incr()

As a wrapper for the C function ydb_lock_incr_st(), lock_incr() acquires a lock not currently held by the process, or increments the count for locks already held.

pub fn lock_incr(&self, timeout: Duration) -> YDBResult<()>

timeout specifies a time that the function waits to acquire the requested locks. If timeout is 0, the function makes exactly one attempt to acquire the lock.

Example:

use yottadb::{Context, KeyContext};
use std::time::Duration;

let ctx = Context::new();
let key = KeyContext::variable(&ctx, "lockIncrTest");
key.lock_incr(Duration::from_secs(1))?;

next_node()

As a wrapper for the C function ydb_node_next_st(), next_node() facilitates traversal of a local or global variable tree to return the next node.

pub fn next_node(&mut self) -> YDBResult<KeyContext>

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0", "0");

    key.set("Hello world!")?;
    // Forget the second subscript
    key.truncate(1);
    let k2 = key.next_node()?;

    assert_eq!(k2[1], b"0");
    println!("Current node : {:?}",key.key);
    println!("Next node: {:?}", k2.key);
    Ok(())
}

Output:

Current node : ^hello("0")
Next node: ^hello("0", "0")

next_node_self()

As a wrapper for the C function ydb_node_next_st(), next_mode_self() facilitates traversal of a local or global variable tree, and passes itself in as the output parameter.

pub fn next_node_self(&mut self) -> YDBResult<()>

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0", "0");

    key.set("Hello world!")?;
    // Forget the second subscript
    key.truncate(1);
    println!("Current node : {:?}",key.key);
    key.next_node_self()?;

    assert_eq!(key[1], b"0");
    println!("Next node : {:?}",key.key);
    Ok(())
}

Output:

Current node : ^hello("0")
Next node : ^hello("0", "0")

next_sub()

As a wrapper for the C function ydb_subscript_next_st(), next_sub() implements traversal of a tree by searching for the next subscript.

pub fn next_sub(&self) -> YDBResult<Vec<u8>>

Example:

use yottadb::Context;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0");

    key.set("Hello world!")?;
    key[0] = Vec::from("1");
    key.set("Hello world!")?;
    key[0] = Vec::from("0");
    let subscript = key.next_sub()?;

    assert_eq!(subscript, b"1");

    Ok(())
}

next_sub_self()

As a wrapper for the C function ydb_subscript_next_st(), next_sub_self() implements traversal of a tree by searching for the next subscript, and passes itself as the output parameter.

pub fn next_sub_self(&mut self) -> YDBResult<()>

Example:

use yottadb::Context;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "a");

    key.set("Hello world!")?;
    key[0] = Vec::from("b");
    key.set("Hello world!")?;
    key[0] = Vec::from("a");
    // Starting at a, the next sub should be b
    key.next_sub_self()?;

    assert_eq!(key[0], b"b");

    Ok(())
}

prev_node()

As a wrapper for the C function ydb_node_previous_st(), prev_node() facilitates reverse traversal of a local or global variable tree to return the previous node.

pub fn prev_node(&mut self) -> YDBResult<KeyContext>

Example:

use yottadb::{Context,make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^helloPrevNode", "0", "0");

    key.set("Hello world!")?;
    // Forget the second subscript
    key.truncate(1);
    // Go to the next node, then walk backward
    key[0] = "1".into();
    let k2 = key.prev_node()?;

    println!("Current node: {:?}",key.key);
    assert_eq!(&k2.variable, "^helloPrevNode");
    assert_eq!(k2[0], b"0");
    assert_eq!(k2[1], b"0");
    println!("Previous node: {:?}",k2.key);
    Ok(())
}

Output:

Current node: ^helloPrevNode("1")
Previous node: ^helloPrevNode("0", "0")

prev_node_self()

As a wrapper for the C function ydb_node_previous_st(), prev_node_self() facilitates reverse traversal of a local or global variable tree and reports the predecessor node, passing itself in as the output parameter.

pub fn prev_node_self(&mut self) -> YDBResult<()>

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0", "0");

    key.set("Hello world!")?;
    // Forget the second subscript
    key.truncate(1);
    println!("Current node: {:?}",key.key);
    // Go to the next node, then walk backward
    key[0] = Vec::from("1");
    key.prev_node_self()?;

    assert_eq!(key[1], b"0");
    println!("Previous node: {:?}",key.key);
    Ok(())
}

Output:

Current node: ^hello("0")
Previous node: ^hello("0", "0")

prev_sub()

As a wrapper for the C function ydb_subscript_previous_st(), prev_sub() implements traversal of a tree by searching for the previous subscript.

pub fn prev_sub(&self) -> YDBResult<Vec<u8>>

Example:

use yottadb::Context;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0");

    key.set(b"Hello world!")?;
    key[0] = Vec::from("1");
    key.set("Hello world!")?;
    key[0] = Vec::from("1");
    let subscript = key.prev_sub()?;

    assert_eq!(subscript, b"0");

    Ok(())
}

prev_sub_self()

As a wrapper for the C function ydb_subscript_previous_st(), prev_sub_self() implements reverse traversal of a tree by searching for the previous subscript, and passes itself in as the output parameter.

pub fn prev_sub_self(&mut self) -> YDBResult<()>

Example:

use yottadb::Context;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let mut key = make_ckey!(ctx, "^hello", "0");

    key.set("Hello world!")?;
    key[0] = Vec::from("1");
    key.set("Hello world!")?;
    key[0] = Vec::from("1");
    key.prev_sub_self()?;

    assert_eq!(key[0], b"0");

    Ok(())
}

set()

As a wrapper for the C function ydb_set_st(), set() sets the value of a key in the database.

pub fn set<U: AsRef<[u8]>>(&self, new_val: U) -> YDBResult<()>

Example:

use yottadb::{Context, make_ckey};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ctx = Context::new();
    let key = make_ckey!(ctx, "^hello");

    key.set("Hello world!")?;
    let output_buffer = key.get()?;

    assert_eq!(output_buffer, b"Hello world!");
    println!("{:?}", String::from_utf8(output_buffer).unwrap());
    Ok(())
}

Output:

"Hello world!"

Comparison between Rust and Go wrappers

  • Rust has almost no overhead calling into C. There is no reallocation of buffers as with Go.

  • Rust has a context API, not an easy API. The difference is that buffers are re-used between calls so it’s not constantly allocating and deallocating like the Go Easy API does, nor does it require passing in the tptoken and error buffer like the Go Simple API.

  • Rust can pass numbers into M FFI, not just ydb_string_t.