Rust SDK Guide (schemabound)
The schemabound crate is the core Rust runtime. It is the reference implementation of the OAM
framework and the foundation that all other SDKs and services build on.
Why Use The Rust SDK Directly
- You are building SCHEMABOUND backend services, proxies, or custom gRPC adapters.
- You need the full execution and introspection surface — not just a client.
- You want zero-overhead integration with Tokio async runtimes.
- You need to implement a custom
MirrorProviderorQueryRuntimeAugmentor.
Installation
Add schemabound to your Cargo.toml:
[dependencies]
schemabound = "0.6"
tokio = { version = "1", features = ["full"] }
Crate Structure
| Module | Purpose |
|---|---|
schemabound::mirror | SQLite schema introspection — tables, columns, indexes, triggers, UDTs, field mappings |
schemabound::executor | SchemaService / QueryService traits + SQLite implementations |
schemabound::grpc_executor | Tonic gRPC server wrapping the executor services |
schemabound::interceptor | Typed event bus — Event enum, EventBus, EventHandler, HandleOutcome |
schemabound::handlers | Built-in CoR handlers — AuditLogHandler, QueryMetricsHandler, SessionActivityHandler, DefaultHandlerChain, SharedHandler |
schemabound::tcp | TCP JSON-RPC transport and per-client auth |
schemabound::policy_engine | Tool-contract policy evaluation and subquery governance |
schemabound::runtime_context | QueryRuntimeContext — the carrier for per-request augmentation metadata |
schemabound::rate_limit | Connection and request rate limiter |
schemabound::mapper | Mapper trait + LocalMapper (SQLite) + TcpMapper (remote) |
Quick Start
Start a gRPC Server
use schemabound::grpc_executor::GrpcExecutor;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let executor = GrpcExecutor::new("path/to/database.db")?;
let handle = executor.start_server("0.0.0.0:50051").await?;
handle.await?;
Ok(())
}
Introspect A SQLite Database
#![allow(unused)]
fn main() {
use schemabound::mirror::introspect_sqlite_path;
let schema = introspect_sqlite_path("path/to/database.db")?;
for table in &schema.tables {
println!("Table: {}", table.name);
for col in &table.columns {
println!(" Column: {} ({}){}", col.name, col.sql_type,
if col.primary_key { " PK" } else { "" });
}
for trigger in &table.triggers {
println!(" Trigger: {} {} {}", trigger.name, trigger.timing, trigger.event);
}
for fm in &table.field_mappings {
println!(" FieldMapping: {} → {} ({})", fm.physical_name, fm.logical_name, fm.orm_convention);
}
}
println!("UDTs: {}", schema.user_defined_types.len());
}
Use The MirrorProvider Trait
MirrorProvider is an async trait for pluggable schema introspection:
#![allow(unused)]
fn main() {
use schemabound::{MirrorProvider, SqliteMirrorProvider};
let provider = SqliteMirrorProvider::new("database.db");
let schema = provider.introspect_schema().await?;
}
Implement the trait to integrate custom databases:
#![allow(unused)]
fn main() {
use schemabound::MirrorProvider;
use schemabound::mirror::SchemaModel;
struct MyCustomProvider;
#[async_trait::async_trait]
impl MirrorProvider for MyCustomProvider {
async fn introspect_schema(&self) -> Result<SchemaModel, String> {
// custom introspection logic
Ok(SchemaModel { tables: vec![], user_defined_types: vec![] })
}
}
}
Register A CoR Handler
Handlers are invoked in registration order for every dispatched event. Use the built-in
collection from schemabound::handlers or implement EventHandler yourself:
#![allow(unused)]
fn main() {
use schemabound::{get_event_bus, AuditLogHandler, DefaultHandlerChain, SharedHandler};
use std::sync::Arc;
// Recommended starting point: audit log first, then metrics/session counters
let chain = DefaultHandlerChain::new();
let bus = get_event_bus();
bus.register_handler(Box::new(AuditLogHandler))?;
bus.register_handler(Box::new(SharedHandler(Arc::clone(&chain.query_metrics))))?;
bus.register_handler(Box::new(SharedHandler(Arc::clone(&chain.session_activity))))?;
// Retain chain handles for health probes
let snap = chain.query_metrics.snapshot();
let sessions = chain.session_activity.session_count();
}
See the Event Pipeline architecture guide for the full handler reference and a comparison with subscribers.
Subscribe To Events (fire-and-forget)
#![allow(unused)]
fn main() {
use schemabound::interceptor::{get_event_bus, Event};
let bus = get_event_bus();
let id = bus.register_subscriber(Box::new(|event: &Event| {
println!("Event: {}", event.event_type());
}))?;
// Unsubscribe when done
bus.unregister_subscriber(id)?;
}
Apply A Policy To A Query
#![allow(unused)]
fn main() {
use schemabound::executor::{QueryServiceImpl, ValidateQueryRequest};
use schemabound::policy_engine::{
AuthorizationContext, AuthorizedSubqueryShape, PolicyContext, SubqueryPolicy, ToolContract, ToolIntent,
};
let mut service = QueryServiceImpl::new();
service.set_db_path("database.db")?;
let request = ValidateQueryRequest {
db_identifier: "db".into(),
query: "SELECT id FROM users WHERE org_id IN (SELECT id FROM organizations)".into(),
parameters: Default::default(),
};
let policy = PolicyContext {
tool: ToolContract {
name: "list-users".into(),
intent: ToolIntent::ReadSelect,
subquery_policy: SubqueryPolicy::AllowListed(vec![
AuthorizedSubqueryShape { table: "organizations".into() },
]),
},
authorization: AuthorizationContext {
allowed_intents: vec![ToolIntent::ReadSelect],
grants: vec!["tool:users.read".into()],
},
};
let response = service.validate_query_with_policy(request, policy).await?;
assert!(response.valid);
}
Schema Introspection Model
The mirror module returns a tree of Rust structs:
SchemaModel
├── tables: Vec<Table>
│ ├── columns: Vec<Column> name, sql_type, nullable, primary_key, default_value
│ ├── indexes: Vec<Index> name, columns
│ ├── unique_indexes: Vec<UniqueIndex>
│ ├── foreign_keys: Vec<ForeignKey>
│ ├── composite_foreign_keys: Vec<CompositeForeignKey>
│ ├── triggers: Vec<Trigger> name, event, timing, table_name, body
│ └── field_mappings: Vec<FieldMapping>
│ └── logical_name, physical_name, orm_convention (Hibernate | EntityFramework)
└── user_defined_types: Vec<UserDefinedType>
└── name, base_type, check_constraint, nullable, default_value
Field mapping convention detection is heuristic:
camelCasecolumn names →HibernatePascalCasecolumn names →EntityFramework(Entity Framework)_prefixedcolumns →EntityFramework(EF Core shadow property convention)
gRPC Proto Mapping
The table_to_table_def function converts a mirror::Table to a proto TableDef:
#![allow(unused)]
fn main() {
use schemabound::grpc_executor::table_to_table_def;
let proto_table = table_to_table_def(&my_mirror_table);
}
Both unique_indexes and indexes are merged into TableDef.indexes, distinguished by
IndexDef.is_unique.
Contributing
See the Contribution Workflow for the full TDD contract — no production code may be written before a failing test exists.
Test Targets
# Unit tests only (fast, no infra)
make test-unit
# Full test suite (schemabound-public)
make test FILTER=schemabound-public
# Proto unit tests
make test-proto