Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Step 6: Multi-Backend Applications

At this point your backend works — you can define tables, query data, and traverse relationships. But a real application typically has a model crate that defines entities once and offers table constructors for each backend. That’s bakery_model3 in the Vantage repo. The final piece is AnyTable, which lets you treat tables from different backends uniformly.

AnyTable: the type-erased wrapper

AnyTable erases the backend and entity types behind a uniform serde_json::Value-based interface. This is what makes it possible to write generic UI, CLI, or API code that doesn’t care which database is behind it.

There are two ways to create one:

#![allow(unused)]
fn main() {
// 1. If your backend already uses serde_json::Value (rare):
let any = AnyTable::new(my_table);

// 2. For backends with custom value types (the common case):
let any = AnyTable::from_table(Product::sqlite_table(db));
}

from_table works as long as your AnyType implements Into<serde_json::Value> and From<serde_json::Value>. The vantage_type_system! macro generates the Into conversion automatically, and after Step 1 your backend should have the From direction covered too.

Building a multi-source CLI

The CLI example in bakery_model3/examples/cli.rs shows the pattern. A build_table function matches on the user’s chosen source, calls the right entity constructor, and wraps it with AnyTable::from_table(). Once you have an AnyTable, all commands are backend-agnostic — list_values(), get_count(), get_some_value(), insert_value(), and delete() all work identically regardless of which database is behind it.

Because the values flow through as serde_json::Value, the CLI renderer can inspect types at runtime — booleans like is_deleted display as true/false with color highlighting, numbers stay numeric, and nulls render cleanly. Your type system work in Step 1 ensures these values arrive with the right JSON type rather than everything being a string.

Try it out:

# List products from CSV
cargo run --example cli -- csv product list

# Same thing from SQLite
cargo run --example cli -- sqlite product list

# Count bakeries in SurrealDB
cargo run --example cli -- surreal bakery count

# Get a single product record
cargo run --example cli -- sqlite product get

# Insert a new record
cargo run --example cli -- surreal bakery add myid '{"name":"Test","profit_margin":10}'

# Delete a record
cargo run --example cli -- surreal bakery delete myid

That’s the payoff of implementing a proper type system and TableSource — one line of AnyTable::from_table() bridges the gap between your backend’s native types and a uniform JSON-based interface.