Metadata Introspection
SCHEMABOUND’s metadata introspection surface — exposed through the schemabound::mirror module — gives agents
a deep, structured view of the underlying database schema. Beyond basic table-and-column enumeration,
it surfaces triggers, user-defined types, and ORM field-mapping heuristics so that
agents can reason about data contracts and change semantics without manual annotation.
What Is Introspected
| Primitive | Struct | Key Fields |
|---|---|---|
| Table | mirror::Table | name, columns, indexes, unique_indexes, foreign_keys, triggers, field_mappings |
| Column | mirror::Column | name, sql_type, nullable, primary_key, default_value, enum_values |
| Non-unique index | mirror::Index | name, columns |
| Unique index | mirror::UniqueIndex | name, columns |
| Foreign key | mirror::ForeignKey | from_column, to_table, to_column, on_delete, on_update |
| Composite FK | mirror::CompositeForeignKey | from_columns, to_table, to_columns, … |
| Trigger | mirror::Trigger | name, event, timing, table_name, body |
| User-defined type | mirror::UserDefinedType | name, base_type, check_constraint, nullable, default_value |
| Field mapping | mirror::FieldMapping | logical_name, physical_name, orm_convention, notes |
The full model is returned as a SchemaModel:
#![allow(unused)]
fn main() {
pub struct SchemaModel {
pub tables: Vec<Table>,
pub user_defined_types: Vec<UserDefinedType>,
}
}
Trigger Discovery
Triggers represent procedural data-change logic embedded in the database. Surfacing them
allows agents to warn about side-effects, model cascading writes, and fire TriggerFired events.
#![allow(unused)]
fn main() {
use schemabound::mirror::introspect_sqlite_path;
let schema = introspect_sqlite_path("path/to/database.db")?;
for table in &schema.tables {
for trigger in &table.triggers {
println!(
"[{}] TRIGGER {} {} {} ON {}",
table.name, trigger.name, trigger.timing, trigger.event, trigger.table_name
);
// timing: BEFORE | AFTER | INSTEAD OF
// event: INSERT | UPDATE | DELETE | UPDATE OF <col>
}
}
}
Trigger Struct
#![allow(unused)]
fn main() {
pub struct Trigger {
pub name: String,
/// INSERT | UPDATE | DELETE | UPDATE OF <col>
pub event: String,
/// BEFORE | AFTER | INSTEAD OF
pub timing: String,
pub table_name: String,
pub body: String, // the raw CREATE TRIGGER body
}
}
TriggerFired Event (EventBus)
When the SCHEMABOUND runtime detects that a mutation query will fire a trigger, it emits a
TriggerFired variant on the EventBus:
#![allow(unused)]
fn main() {
use schemabound::interceptor::{get_event_bus, Event};
get_event_bus().register_subscriber(Box::new(|event: &Event| {
if let Event::TriggerFired { trigger_name, table_name, .. } = event {
log::warn!("Trigger {} fired on {}", trigger_name, table_name);
}
}))?;
}
User-Defined Types
SQLite encodes custom types as columns with CHECK constraints. The introspector discovers
these and promotes them to first-class UserDefinedType entries.
#![allow(unused)]
fn main() {
pub struct UserDefinedType {
pub name: String,
pub base_type: String, // TEXT, INTEGER, REAL, …
pub check_constraint: Option<String>,
pub nullable: bool,
pub default_value: Option<String>,
}
}
Example — a table with an enum-like column:
CREATE TABLE orders (
status TEXT NOT NULL CHECK(status IN ('pending','shipped','cancelled'))
);
The introspector produces:
{
"name": "status",
"base_type": "TEXT",
"check_constraint": "status IN ('pending','shipped','cancelled')",
"nullable": false,
"default_value": null
}
This is also surfaced per-column via Column.enum_values.
ORM Field-Mapping Heuristics
Physical column names often differ from the logical names that application layers — Hibernate,
Entity Framework, ActiveRecord — use. SCHEMABOUND detects the convention from naming patterns and
generates a FieldMapping per column that appears to be ORM-managed.
#![allow(unused)]
fn main() {
pub struct FieldMapping {
pub logical_name: String,
pub physical_name: String,
/// "hibernate" | "ef" | "ef_shadow"
pub orm_convention: String,
pub notes: Option<String>,
}
}
Convention Detection Rules
| Pattern | Convention | Example |
|---|---|---|
camelCase | hibernate | orderId → order_id |
PascalCase | ef | OrderId → order_id |
_prefix | ef_shadow | _tenantId → TenantId |
These heuristics enable agents to translate between LLM-generated column names and physical database column names without user annotations.
The MirrorProvider Trait
The MirrorProvider async trait lets you plug in any database backend:
#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait MirrorProvider: Send + Sync {
async fn introspect_schema(&self) -> Result<SchemaModel, String>;
}
}
The built-in implementation is SqliteMirrorProvider:
#![allow(unused)]
fn main() {
use schemabound::{MirrorProvider, SqliteMirrorProvider};
let provider = SqliteMirrorProvider::new("path/to/database.db");
let schema = provider.introspect_schema().await?;
}
To extend SCHEMABOUND to a new database engine, implement MirrorProvider and register the provider
with the GrpcExecutor builder:
#![allow(unused)]
fn main() {
let executor = GrpcExecutor::builder()
.mirror(MyPostgresMirrorProvider::new(&connection_string))
.build()?;
}
Proto Surface
The introspected metadata is exposed over gRPC via GetSchemaResponse and GetTableResponse
(see SchemaService proto):
message GetSchemaResponse {
string schema_id = 1;
string database_type = 2;
string generated_at = 3;
repeated TableDef tables = 4;
repeated UserDefinedTypeDef user_defined_types = 5;
}
message GetTableResponse {
string generated_at = 1;
TableDef table = 2;
}
message TableDef {
string name = 1;
repeated ColumnDef columns = 2;
repeated IndexDef indexes = 3; // is_unique distinguishes unique from regular
repeated TriggerDef triggers = 4;
repeated FieldMappingDef field_mappings = 5;
}
message TriggerDef {
string name = 1;
string timing = 2;
string event = 3;
string body = 4;
}
message UserDefinedTypeDef {
string name = 1;
string base_type = 2;
repeated string variants = 3;
}
message FieldMappingDef {
string column_name = 1;
string logical_name = 2;
string convention = 3;
}
The Rust conversion function is table_to_table_def:
#![allow(unused)]
fn main() {
use schemabound::grpc_executor::table_to_table_def;
let proto_def = table_to_table_def(&schema.tables[0]);
}
Future Work
- MSSQL introspector —
MssqlMirrorProviderfor SQL Server schemas, complex UDTs, CLR triggers - PostgreSQL introspector — domain types, row-level security policies, event triggers
- Computed column detection — flag virtual/generated columns explicitly
- Partition metadata — surface range/list/hash partitioning from supported engines