Simple command line phonebook with SurrealDB using SQL

SurrealDB SQL

This is an implementation of a simple phonebook for the command line that uses an embedded SurrealDB databse with a RocksDb storage using SQL.

It seems that there are so many ways to use SurrealDB that we need that many words to describe the particular implementation we have.

SurrealDB can be used either as a stand-along database server

examples/surrealdb/cli-phone-book-with-embedded-rocksdb/Cargo.toml

[package]
name = "phonebook"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0.193", features = ["derive"] }
surrealdb = { version = "1.0.2", features = ["kv-rocksdb"] }
tempdir = "0.3.7"
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }

examples/surrealdb/cli-phone-book-with-embedded-rocksdb/src/main.rs

use serde::{Deserialize, Serialize};
use surrealdb::engine::local::RocksDb;
use surrealdb::Surreal;

#[derive(Debug, Deserialize, Serialize)]
struct Entry {
    name: String,
    phone: String,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let args = std::env::args().collect::<Vec<String>>();

    let database_folder = match std::env::var("DATABASE_PATH") {
        Ok(val) => std::path::PathBuf::from(val),
        Err(_) => {
            let current_dir = std::env::current_dir().unwrap();
            current_dir.join("phonebook.db")
        }
    };

    let db = Surreal::new::<RocksDb>(database_folder).await?;

    db.use_ns("test").use_db("test").await?;

    // Maybe do this only when we create the database
    let _response = db
        .query("DEFINE INDEX entry_email ON TABLE entry COLUMNS name UNIQUE")
        .await?;

    if args.len() == 4 {
        let command = &args[1];
        if command == "add" {
            let name = &args[2];
            let phone = &args[3];

            let response = db
                .query("CREATE entry SET  name=$name, phone=$phone")
                .bind(("name", name))
                .bind(("phone", phone))
                .await?;
            //println!("{:?}", response);

            match response.check() {
                Ok(_) => return Ok(()),
                Err(err) => {
                    eprintln!("Could not add entry {}", err);
                    std::process::exit(2);
                }
            };
        }
    }

    if args.len() == 2 {
        let command = &args[1];
        if command == "list" {
            let mut entries = db
                .query("SELECT name, phone FROM type::table($table) ORDER BY name ASC")
                .bind(("table", "entry"))
                .await?;
            let entries: Vec<Entry> = entries.take(0)?;
            for entry in entries {
                println!("{}: {}", entry.name, entry.phone);
            }

            return Ok(());
        }
    }

    if args.len() == 3 {
        let command = &args[1];
        if command == "show" {
            let name = &args[2];

            let mut entries = db
                .query("SELECT name, phone FROM type::table($table) WHERE name=$name")
                .bind(("table", "entry"))
                .bind(("name", name))
                .await?;
            let entries: Vec<Entry> = entries.take(0)?;
            for entry in entries {
                println!("{}: {}", entry.name, entry.phone);
            }

            return Ok(());
        }

        if command == "delete" {
            let name = &args[2];

            let _response = db
                .query("DELETE FROM type::table($table) WHERE name=$name")
                .bind(("table", "entry"))
                .bind(("name", name))
                .await?;

            return Ok(());
        }
    }

    eprintln!("Usage: {} add name value", args[0]);
    std::process::exit(1);
}

examples/surrealdb/cli-phone-book-with-embedded-rocksdb/tests/test_empty.rs

use std::{
    os::unix::process::ExitStatusExt,
    process::{Command, ExitStatus},
};
use tempdir::TempDir;

#[test]
fn test_phonebook_no_params() {
    let tmp_dir = TempDir::new("phonebook").unwrap();
    println!("tmp_dir: {:?}", tmp_dir);

    std::env::set_var("DATABASE_PATH", tmp_dir.path());
    let result = Command::new("cargo")
        .args(["run", "-q"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "");
    assert_eq!(
        std::str::from_utf8(&result.stderr).unwrap(),
        "Usage: target/debug/phonebook add name value\n"
    );
    assert_eq!(result.status, ExitStatus::from_raw(1 * 256));
}

examples/surrealdb/cli-phone-book-with-embedded-rocksdb/tests/tests.rs

use std::{
    os::unix::process::ExitStatusExt,
    process::{Command, ExitStatus},
};
use tempdir::TempDir;

#[test]
fn test_phonebook() {
    let tmp_dir = TempDir::new("phonebook").unwrap();
    println!("tmp_dir: {:?}", tmp_dir);

    std::env::set_var("DATABASE_PATH", tmp_dir.path());

    let result = Command::new("cargo")
        .args(["run", "-q", "add", "foo", "123"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "add", "bar", "456"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "add", "foo", "789"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "");
    assert!(std::str::from_utf8(&result.stderr).unwrap().contains(
        "Could not add entry Database index `entry_email` already contains 'foo', with record"
    ));
    assert_eq!(result.status, ExitStatus::from_raw(2 * 256));

    let result = Command::new("cargo")
        .args(["run", "-q", "show", "foo"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "foo: 123\n");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "show", "bar"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "bar: 456\n");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "list"])
        .output()
        .expect("command failed to start");

    //println!("{}", std::str::from_utf8(&result.stdout).unwrap());
    assert_eq!(
        std::str::from_utf8(&result.stdout).unwrap(),
        "bar: 456\nfoo: 123\n"
    );
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "delete", "foo"])
        .output()
        .expect("command failed to start");

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    let result = Command::new("cargo")
        .args(["run", "-q", "list"])
        .output()
        .expect("command failed to start");

    //println!("{}", std::str::from_utf8(&result.stdout).unwrap());
    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "bar: 456\n");
    assert_eq!(std::str::from_utf8(&result.stderr).unwrap(), "");
    assert_eq!(result.status, ExitStatus::from_raw(0));

    drop(tmp_dir);
}

Related Pages

SurrealDB

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo