1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Session SQL Database types and trait
//
// Types and async trait for session-scoped SQL databases.
// Implementations live in the session-sqldb crate.
//
// EVE-897: this module moved out of core with its trait. The value types are
// the signature vocabulary of `SessionSqlDbStore`, so records and trait travel
// together; what pinned them to the kernel was `ToolContext::sqldb_store`.
// That field is gone — the capability now resolves the store as a typed
// extension — so the whole family lives beside the capability that uses it.
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::typed_id::SessionId;
/// Metadata about a session database (no content/pages).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseInfo {
pub name: String,
pub size_bytes: i64,
pub page_count: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Result of a SELECT query.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SqlQueryResult {
pub columns: Vec<String>,
pub rows: Vec<Vec<serde_json::Value>>,
pub row_count: usize,
/// True if results were truncated due to limits
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
}
/// Result of an INSERT/UPDATE/DELETE/DDL statement.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SqlExecuteResult {
pub rows_affected: u64,
}
/// Schema of a table.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchema {
pub name: String,
pub columns: Vec<ColumnSchema>,
pub row_count: i64,
}
/// Schema of a column.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnSchema {
pub name: String,
#[serde(rename = "type")]
pub column_type: String,
pub notnull: bool,
pub pk: bool,
pub default_value: Option<String>,
}
/// Error type for session SQL database operations.
///
/// Tool-visible errors (validation, not found, limits) vs internal errors
/// are distinguished by the caller when converting to ToolExecutionResult.
#[derive(Debug, thiserror::Error)]
pub enum SessionSqlDbError {
#[error("database not found: {0}")]
DatabaseNotFound(String),
#[error("database already exists: {0}")]
DatabaseAlreadyExists(String),
#[error("invalid database name: {0}")]
InvalidDatabaseName(String),
#[error("database limit exceeded: {0}")]
LimitExceeded(String),
#[error("query error: {0}")]
QueryError(String),
#[error("query timeout after {0} seconds")]
QueryTimeout(u64),
#[error("result too large: {0}")]
ResultTooLarge(String),
#[error("operation blocked by authorizer: {0}")]
AuthorizerBlocked(String),
#[error("internal error: {0}")]
Internal(String),
}
impl SessionSqlDbError {
/// Whether this error should be shown to the LLM as a tool error (vs internal error).
pub fn is_tool_error(&self) -> bool {
matches!(
self,
Self::DatabaseNotFound(_)
| Self::DatabaseAlreadyExists(_)
| Self::InvalidDatabaseName(_)
| Self::LimitExceeded(_)
| Self::QueryError(_)
| Self::QueryTimeout(_)
| Self::ResultTooLarge(_)
| Self::AuthorizerBlocked(_)
)
}
}
/// Async trait for session-scoped SQL database operations.
///
/// Implementations:
/// - InMemorySqlDbBackend (DEV_MODE) in session-sqldb crate
/// - Future: PostgreSQL VFS backend
#[async_trait]
pub trait SessionSqlDbStore: Send + Sync {
/// Create a named database in the session.
async fn create_database(
&self,
session_id: SessionId,
name: &str,
) -> Result<DatabaseInfo, SessionSqlDbError>;
/// List all databases for a session.
async fn list_databases(
&self,
session_id: SessionId,
) -> Result<Vec<DatabaseInfo>, SessionSqlDbError>;
/// Get metadata for a specific database.
async fn get_database(
&self,
session_id: SessionId,
name: &str,
) -> Result<Option<DatabaseInfo>, SessionSqlDbError>;
/// Delete a database and all its pages.
async fn delete_database(
&self,
session_id: SessionId,
name: &str,
) -> Result<bool, SessionSqlDbError>;
/// Execute DDL/DML SQL. Auto-creates database if it doesn't exist.
async fn sql_execute(
&self,
session_id: SessionId,
db_name: &str,
sql: &str,
) -> Result<SqlExecuteResult, SessionSqlDbError>;
/// Execute read-only SQL query. Returns columns and rows.
async fn sql_query(
&self,
session_id: SessionId,
db_name: &str,
sql: &str,
) -> Result<SqlQueryResult, SessionSqlDbError>;
/// Get schema for all tables (or a specific table) in a database.
async fn sql_schema(
&self,
session_id: SessionId,
db_name: &str,
table: Option<&str>,
) -> Result<Vec<TableSchema>, SessionSqlDbError>;
}
/// Type-keyed wrapper installed on the tool context by hosted presets.
///
/// Core carries the generic extension bag but does not name this service: the
/// session SQL database is a hosted capability's backend, so the capability and
/// its store contract stay together in platform (EVE-897).
#[derive(Clone)]
pub struct SessionSqlDbStoreExt(pub std::sync::Arc<dyn SessionSqlDbStore>);