In past blog posts, we demonstrated how to use the ABAP ODBC driver with client programs like Excel and LibreOffice, and programming languages like Python (see links below).
In this blog post, I’ll show how to employ the ABAP ODBC driver within a straightforward Rust program.
Rust compatibility begins with version 01.00.0018, but we recommend using the latest version of the driver for optimal performance and features.
To get started, set up a basic Rust environment following the instructions in the first chapter of the official Rust documentation.
This blog post uses version 1.86.0 (05f9846f8 2025-03-31) of rustc.
Begin by creating a new Rust program.
cargo new abap_odbc
Next, add the crates anyhow, text_io and rpassword to your program.
cd abap_odbc
cargo add anyhow text_io rpassword
The most crucial crate is odbc-api.
The ABAP ODBC driver does not support newer ODBC features, so you need to limit the odbc-api crate to older versions by using the feature odbc_version_3_5.
cargo add odbc-api –features odbc_version_3_5
The Cargo.toml file should now resemble the following:
[package]
name = “abap_odbc”
version = “0.1.0”
edition = “2024”
[dependencies]
anyhow = “1.0.98”
odbc-api = { version = “13.0.1”, features = [“odbc_version_3_5”] }
rpassword = “7.4.0”
text_io = “0.1.13”
Proceed to insert the code for a basic program into the main.rs file.
Ensure you have a data source that is already configured in the ODBC driver manager, along with a username and password.
For further setup guidance, refer to the blog post discussing driver setup on Linux or the blog post discussing driver setup on Windows.
Start with a main function, where you can add function calls for specific examples later:
use anyhow::Error;
use odbc_api::{ConnectionOptions, Cursor, Environment, IntoParameter, ResultSetMetadata, buffers::TextRowSet};
use rpassword::read_password;
use text_io::read;
fn main() -> Result<(), Error> {
println!(“DSN:”);
let dsn: String = read!();
println!(“user:”);
let user: String = read!();
println!(“password:”);
let password = read_password().unwrap();
let environment = Environment::new()?;
let connection = environment.connect(&dsn, &user, &password, ConnectionOptions::default())?;
// add the function calls to the specific examples here
Ok(())
}
Run the program using:
cargo run
Depending on your setup, you may need to take additional steps to ensure that all shared libraries are found, like setting the LD_LIBARARY_PATH variable on Linux.
Initially, the compiler will issue warnings about unused imports.
At this stage, the program will prompt for the data source name (DSN), username, and password, and establish a connection.
The program should end without any additional output.
Next, incorporate a function that performs a simple SELECT statement and invoke it (simple_select(&connection)?;) from the main() function.
fn simple_select(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “select * from sys.dummy”;
match connection.execute(sql, (), None)? {
Some(mut cursor) => {
if let Some(mut row) = cursor.next_row()? {
let mut buf = Vec::new();
row.get_text(1, &mut buf)?;
let ret = String::from_utf8(buf).unwrap();
println!(“{:?}”, ret);
}
}
None => {
eprintln!(“empty result”);
}
}
Ok(())
}
This function reads the sys.dummy view and outputs its contents to the console.
Since sys.dummy is the ubiquitous default object, currently, there’s no necessity for actual tables or views in the system.
To maintain simplicity, the program only prints the first column from the first row of the result.
Conveniently, that’s all that exists in sys.dummy.
Upon executing with cargo run, the output should be:
“X”
While this example provides limited functionality, you can now add a more complex one with a procedure call.
Our blog post Expose and Use ABAP-Managed Database Procedures (AMDPs) in SQL Services serves as a prerequisite for the next example since it employs the same database procedures.
Follow the instructions in the post to set up the required objects in the ABAP system.
Since the procedure requires an input table, create a local temporary table and populate it with data:
fn create_temp_table(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “create local temporary table sys_temp.items_in (item char(100) not null) on commit preserve rows”;
connection.execute(sql, (), None)?;
let inputs_for_tab = &[“Apple”, “Orange”, “Pear”];
for input_for_tab in inputs_for_tab {
connection.execute(
“insert into sys_temp.items_in values(?)”,
&input_for_tab.into_parameter(),
None,
)?;
}
Ok(())
}
Once all inputs are prepared, proceed to call the procedure:
fn procedure_call(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “call zfmhorders.testprocedure(min_creation_date => ?, items_tab_in => sys_temp.items_in, id_tab_out => RESULT, items_tab_out => RESULT)”;
let min_creation_date = “20000101”;
match connection.execute(sql, &min_creation_date.into_parameter(), None)? {
Some(mut cursor) => {
println!(“table result:”);
loop {
let headline : Vec<String> = cursor.column_names()?.collect::<Result<_,_>>()?;
println!(“{}”, headline.join(“, “));
let mut buffers = TextRowSet::for_cursor(500, &mut cursor, None)?;
let mut row_set_cursor = cursor.bind_buffer(&mut buffers)?;
while let Some(batch) = row_set_cursor.fetch()? {
for row_index in 0..batch.num_rows() {
let field = batch.at(0, row_index).unwrap_or(&[]);
print!(“{}”, String::from_utf8_lossy(field));
for col_index in 1..batch.num_cols() {
let field = batch.at(col_index, row_index).unwrap_or(&[]);
print!(“, {}”, String::from_utf8_lossy(field));
}
print!(“n”);
}
}
cursor = (row_set_cursor.unbind()?).0;
if let Some(new_cursor) = cursor.more_results()? {
println!(“nnext table result:”);
cursor = new_cursor;
} else {
break;
}
}
}
None => { eprintln!(“empty result”); }
}
Ok(())
}
When the procedure call returns a result cursor (Some(mut cursor)), the function prints a table header with column names (header) and retrieves data from the cursor in 500 byte segments (cursor.bind_buffer(…), row_set_cursor.fetch()).
The data is then displayed in rows (batch.num_rows()) and columns (batch.num_cols()), and the loop continues to the next result table (cursor.more_results()).
Add both function calls to the main() function.
create_temp_table(&connection)?;
procedure_call(&connection)?;
The output should be:
table result:
ID, CREATION_DATE
0000000001, 2021-08-01
0000000002, 2021-08-02
0000000003, 2021-08-03
next table result:
POS, ITEM, AMOUNT
1, Apple, 5
1, Orange, 10
2, Apple, 5
While much of the content in this post is applicable universally when using the odbc-api crate, remember to limit the crate to older ODBC versions by employing the odbc_version_3_5 feature.
Related Blog Posts
Consuming CDS View Entities Using ODBC-Based Client Tools
Using the ODBC driver for ABAP on Linux
SQL Queries on CDS objects Exposed as SQL Service
Data Science with SAP S/4HANA – How to connect HANA-ML with Jupyter Notebooks (Python)
Expose and Use ABAP-Managed Database Procedures (AMDPs) in SQL Services
In past blog posts, we demonstrated how to use the ABAP ODBC driver with client programs like Excel and LibreOffice, and programming languages like Python (see links below).In this blog post, I’ll show how to employ the ABAP ODBC driver within a straightforward Rust program.Rust compatibility begins with version 01.00.0018, but we recommend using the latest version of the driver for optimal performance and features.To get started, set up a basic Rust environment following the instructions in the first chapter of the official Rust documentation.This blog post uses version 1.86.0 (05f9846f8 2025-03-31) of rustc.Begin by creating a new Rust program.cargo new abap_odbcNext, add the crates anyhow, text_io and rpassword to your program.cd abap_odbc
cargo add anyhow text_io rpasswordThe most crucial crate is odbc-api.The ABAP ODBC driver does not support newer ODBC features, so you need to limit the odbc-api crate to older versions by using the feature odbc_version_3_5.cargo add odbc-api –features odbc_version_3_5The Cargo.toml file should now resemble the following:[package]
name = “abap_odbc”
version = “0.1.0”
edition = “2024”
[dependencies]
anyhow = “1.0.98”
odbc-api = { version = “13.0.1”, features = [“odbc_version_3_5”] }
rpassword = “7.4.0”
text_io = “0.1.13”Proceed to insert the code for a basic program into the main.rs file.Ensure you have a data source that is already configured in the ODBC driver manager, along with a username and password.For further setup guidance, refer to the blog post discussing driver setup on Linux or the blog post discussing driver setup on Windows.Start with a main function, where you can add function calls for specific examples later:use anyhow::Error;
use odbc_api::{ConnectionOptions, Cursor, Environment, IntoParameter, ResultSetMetadata, buffers::TextRowSet};
use rpassword::read_password;
use text_io::read;
fn main() -> Result<(), Error> {
println!(“DSN:”);
let dsn: String = read!();
println!(“user:”);
let user: String = read!();
println!(“password:”);
let password = read_password().unwrap();
let environment = Environment::new()?;
let connection = environment.connect(&dsn, &user, &password, ConnectionOptions::default())?;
// add the function calls to the specific examples here
Ok(())
}Run the program using:cargo runDepending on your setup, you may need to take additional steps to ensure that all shared libraries are found, like setting the LD_LIBARARY_PATH variable on Linux.Initially, the compiler will issue warnings about unused imports.At this stage, the program will prompt for the data source name (DSN), username, and password, and establish a connection.The program should end without any additional output.Next, incorporate a function that performs a simple SELECT statement and invoke it (simple_select(&connection)?;) from the main() function.fn simple_select(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “select * from sys.dummy”;
match connection.execute(sql, (), None)? {
Some(mut cursor) => {
if let Some(mut row) = cursor.next_row()? {
let mut buf = Vec::new();
row.get_text(1, &mut buf)?;
let ret = String::from_utf8(buf).unwrap();
println!(“{:?}”, ret);
}
}
None => {
eprintln!(“empty result”);
}
}
Ok(())
}This function reads the sys.dummy view and outputs its contents to the console.Since sys.dummy is the ubiquitous default object, currently, there’s no necessity for actual tables or views in the system.To maintain simplicity, the program only prints the first column from the first row of the result.Conveniently, that’s all that exists in sys.dummy.Upon executing with cargo run, the output should be:”X”While this example provides limited functionality, you can now add a more complex one with a procedure call.Our blog post Expose and Use ABAP-Managed Database Procedures (AMDPs) in SQL Services serves as a prerequisite for the next example since it employs the same database procedures.Follow the instructions in the post to set up the required objects in the ABAP system.Since the procedure requires an input table, create a local temporary table and populate it with data:fn create_temp_table(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “create local temporary table sys_temp.items_in (item char(100) not null) on commit preserve rows”;
connection.execute(sql, (), None)?;
let inputs_for_tab = &[“Apple”, “Orange”, “Pear”];
for input_for_tab in inputs_for_tab {
connection.execute(
“insert into sys_temp.items_in values(?)”,
&input_for_tab.into_parameter(),
None,
)?;
}
Ok(())
}Once all inputs are prepared, proceed to call the procedure:fn procedure_call(connection: &odbc_api::Connection) -> Result<(), Error> {
let sql = “call zfmhorders.testprocedure(min_creation_date => ?, items_tab_in => sys_temp.items_in, id_tab_out => RESULT, items_tab_out => RESULT)”;
let min_creation_date = “20000101”;
match connection.execute(sql, &min_creation_date.into_parameter(), None)? {
Some(mut cursor) => {
println!(“table result:”);
loop {
let headline : Vec<String> = cursor.column_names()?.collect::<Result<_,_>>()?;
println!(“{}”, headline.join(“, “));
let mut buffers = TextRowSet::for_cursor(500, &mut cursor, None)?;
let mut row_set_cursor = cursor.bind_buffer(&mut buffers)?;
while let Some(batch) = row_set_cursor.fetch()? {
for row_index in 0..batch.num_rows() {
let field = batch.at(0, row_index).unwrap_or(&[]);
print!(“{}”, String::from_utf8_lossy(field));
for col_index in 1..batch.num_cols() {
let field = batch.at(col_index, row_index).unwrap_or(&[]);
print!(“, {}”, String::from_utf8_lossy(field));
}
print!(“n”);
}
}
cursor = (row_set_cursor.unbind()?).0;
if let Some(new_cursor) = cursor.more_results()? {
println!(“nnext table result:”);
cursor = new_cursor;
} else {
break;
}
}
}
None => { eprintln!(“empty result”); }
}
Ok(())
}When the procedure call returns a result cursor (Some(mut cursor)), the function prints a table header with column names (header) and retrieves data from the cursor in 500 byte segments (cursor.bind_buffer(…), row_set_cursor.fetch()).The data is then displayed in rows (batch.num_rows()) and columns (batch.num_cols()), and the loop continues to the next result table (cursor.more_results()).Add both function calls to the main() function. create_temp_table(&connection)?;
procedure_call(&connection)?;The output should be:table result:ID, CREATION_DATE0000000001, 2021-08-010000000002, 2021-08-020000000003, 2021-08-03next table result:POS, ITEM, AMOUNT1, Apple, 51, Orange, 102, Apple, 5While much of the content in this post is applicable universally when using the odbc-api crate, remember to limit the crate to older ODBC versions by employing the odbc_version_3_5 feature.Related Blog PostsConsuming CDS View Entities Using ODBC-Based Client ToolsUsing the ODBC driver for ABAP on LinuxSQL Queries on CDS objects Exposed as SQL ServiceData Science with SAP S/4HANA – How to connect HANA-ML with Jupyter Notebooks (Python)Expose and Use ABAP-Managed Database Procedures (AMDPs) in SQL Services Read More Technology Blog Posts by SAP articles
#SAP
#SAPTechnologyblog