Skip to main content

teaql_data_service/
lib.rs

1#![allow(async_fn_in_trait)]
2
3use std::time::SystemTime;
4use teaql_core::{
5    CompactRow, DeleteCommand, EntitySnapshot, GeneratedValues, InsertCommand, RecoverCommand,
6    SelectQuery, TraceNode, UpdateCommand,
7};
8
9#[derive(Debug, Clone, Default)]
10pub struct DataServiceCapabilities {
11    pub query: bool,
12    pub mutation: bool,
13    pub transaction: bool,
14    pub schema: bool,
15    pub id_generation: bool,
16    pub batch_mutation: bool,
17    pub returning: bool,
18}
19
20#[derive(Debug, Clone)]
21pub struct QueryRequest {
22    pub query: SelectQuery,
23    pub trace_chain: Vec<TraceNode>,
24    pub comment: Option<String>,
25    /// Build a copy-paste representation with bind values interpolated.
26    /// Runtimes should disable this when query logging is disabled.
27    pub capture_debug_query: bool,
28}
29
30#[derive(Debug, Clone)]
31pub struct QueryResult {
32    pub rows: Vec<CompactRow>,
33    pub metadata: ExecutionMetadata,
34}
35
36#[derive(Debug, Clone)]
37pub enum MutationRequest {
38    Insert(InsertCommand),
39    Update(UpdateCommand),
40    Delete(DeleteCommand),
41    Recover(RecoverCommand),
42    Batch(Vec<MutationRequest>),
43}
44
45impl MutationRequest {
46    pub fn trace_chain(&self) -> &[teaql_core::TraceNode] {
47        match self {
48            MutationRequest::Insert(cmd) => &cmd.trace_chain,
49            MutationRequest::Update(cmd) => &cmd.trace_chain,
50            MutationRequest::Delete(cmd) => &cmd.trace_chain,
51            MutationRequest::Recover(cmd) => &cmd.trace_chain,
52            MutationRequest::Batch(_) => &[], // Batch traces are per-item
53        }
54    }
55
56    pub fn comment(&self) -> Option<&str> {
57        match self {
58            MutationRequest::Insert(cmd) => cmd.trace_chain.last().map(|n| n.comment.as_str()),
59            MutationRequest::Update(cmd) => cmd.trace_chain.last().map(|n| n.comment.as_str()),
60            MutationRequest::Delete(cmd) => cmd.trace_chain.last().map(|n| n.comment.as_str()),
61            MutationRequest::Recover(cmd) => cmd.trace_chain.last().map(|n| n.comment.as_str()),
62            MutationRequest::Batch(_) => None,
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct MutationResult {
69    pub affected_rows: u64,
70    pub generated_values: GeneratedValues,
71    pub persisted_snapshot: Option<EntitySnapshot>,
72    pub metadata: ExecutionMetadata,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum DataServiceOperation {
77    Query,
78    Insert,
79    Update,
80    Delete,
81    Recover,
82    Batch,
83    Schema,
84}
85
86#[derive(Debug, Clone)]
87pub struct ExecutionMetadata {
88    pub backend: String,
89    pub operation: DataServiceOperation,
90    pub started_at: SystemTime,
91    pub ended_at: SystemTime,
92    pub affected_rows: Option<u64>,
93    pub result_count: Option<usize>,
94    pub trace_chain: Vec<TraceNode>,
95    pub comment: Option<String>,
96    pub backend_request_id: Option<String>,
97    /// Provider-native parameterized request text. For SQL this contains
98    /// placeholders and never interpolated bind values.
99    pub parameterized_query: Option<String>,
100    /// Structured bind values corresponding to `parameterized_query`.
101    pub params: Vec<teaql_core::Value>,
102    pub debug_query: Option<String>,
103}
104
105pub trait DataServiceExecutor {
106    type Error: std::error::Error + Send + Sync + 'static;
107
108    fn capabilities(&self) -> DataServiceCapabilities;
109}
110
111pub trait QueryExecutor: DataServiceExecutor {
112    fn query(
113        &self,
114        request: QueryRequest,
115    ) -> impl std::future::Future<Output = Result<QueryResult, Self::Error>> + Send;
116}
117
118/// Result of a single streaming chunk.
119#[derive(Debug, Clone)]
120pub struct StreamChunk {
121    pub rows: Vec<CompactRow>,
122    pub chunk_index: usize,
123    pub is_last: bool,
124}
125
126pub type QueryStream<'a, E> =
127    std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<StreamChunk, E>> + 'a>>;
128
129/// Streaming query executor. Returns rows in chunks rather than all at once.
130pub trait StreamQueryExecutor: DataServiceExecutor {
131    fn query_stream(
132        &self,
133        request: QueryRequest,
134        chunk_size: usize,
135    ) -> QueryStream<'_, Self::Error>;
136}
137
138pub trait MutationExecutor: DataServiceExecutor {
139    fn mutate(
140        &self,
141        request: MutationRequest,
142    ) -> impl std::future::Future<Output = Result<MutationResult, Self::Error>> + Send;
143}
144
145pub trait TransactionExecutor: DataServiceExecutor {
146    type Tx<'a>: QueryExecutor<Error = Self::Error>
147        + MutationExecutor<Error = Self::Error>
148        + Transaction<Error = Self::Error>
149    where
150        Self: 'a;
151
152    fn begin(&self) -> impl std::future::Future<Output = Result<Self::Tx<'_>, Self::Error>> + Send;
153}
154
155pub trait Transaction {
156    type Error: std::error::Error + Send + Sync + 'static;
157
158    fn commit(self) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
159    fn rollback(self) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
160}
161
162#[derive(Debug, Clone)]
163pub struct SchemaRequest {
164    pub entity_name: String,
165}
166
167#[derive(Debug, Clone)]
168pub struct SchemaResult {
169    pub changed: bool,
170}
171
172pub trait SchemaExecutor: DataServiceExecutor {
173    fn ensure_schema(
174        &self,
175        request: SchemaRequest,
176    ) -> impl std::future::Future<Output = Result<SchemaResult, Self::Error>> + Send;
177}
178
179pub trait IdGeneratorExecutor: DataServiceExecutor {
180    fn next_id(
181        &self,
182        entity: &str,
183    ) -> impl std::future::Future<Output = Result<u64, Self::Error>> + Send;
184}
185
186pub trait SchemaProvider: Send + Sync {
187    fn get_entity(&self, name: &str) -> Option<std::sync::Arc<teaql_core::EntityDescriptor>>;
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_mutation_request_trace_and_comment_accessors() {
196        let trace1 = TraceNode {
197            entity_type: "User".to_string(),
198            entity_id: Some(1),
199            comment: "Create User".to_string(),
200        };
201        let trace2 = TraceNode {
202            entity_type: "Profile".to_string(),
203            entity_id: None,
204            comment: "Create Profile".to_string(),
205        };
206        let trace_chain = vec![trace1.clone(), trace2.clone()];
207
208        // Test Insert
209        let insert_cmd = InsertCommand {
210            entity: "User".to_string(),
211            values: teaql_core::MutationValues::new(),
212            trace_chain: trace_chain.clone(),
213        };
214        let req_insert = MutationRequest::Insert(insert_cmd);
215        assert_eq!(req_insert.trace_chain().len(), 2);
216        assert_eq!(req_insert.trace_chain()[1], trace2);
217        assert_eq!(req_insert.comment(), Some("Create Profile"));
218
219        // Test Update
220        let update_cmd = UpdateCommand {
221            entity: "User".to_string(),
222            id: teaql_core::Value::I64(1),
223            values: teaql_core::MutationValues::new(),
224            expected_version: None,
225            old_values: None,
226            trace_chain: trace_chain.clone(),
227        };
228        let req_update = MutationRequest::Update(update_cmd);
229        assert_eq!(req_update.trace_chain().len(), 2);
230        assert_eq!(req_update.comment(), Some("Create Profile"));
231
232        // Test Delete
233        let delete_cmd = DeleteCommand {
234            entity: "User".to_string(),
235            id: teaql_core::Value::I64(1),
236            expected_version: None,
237            soft_delete: true,
238            trace_chain: trace_chain.clone(),
239        };
240        let req_delete = MutationRequest::Delete(delete_cmd);
241        assert_eq!(req_delete.trace_chain().len(), 2);
242        assert_eq!(req_delete.comment(), Some("Create Profile"));
243
244        // Test Recover
245        let recover_cmd = RecoverCommand {
246            entity: "User".to_string(),
247            id: teaql_core::Value::I64(1),
248            expected_version: 1,
249            trace_chain: trace_chain.clone(),
250        };
251        let req_recover = MutationRequest::Recover(recover_cmd);
252        assert_eq!(req_recover.trace_chain().len(), 2);
253        assert_eq!(req_recover.comment(), Some("Create Profile"));
254
255        // Test Batch
256        let req_batch = MutationRequest::Batch(vec![req_insert, req_update]);
257        assert_eq!(req_batch.trace_chain().len(), 0);
258        assert_eq!(req_batch.comment(), None);
259
260        // Test empty trace chain
261        let insert_empty = InsertCommand {
262            entity: "User".to_string(),
263            values: teaql_core::MutationValues::new(),
264            trace_chain: vec![],
265        };
266        let req_empty = MutationRequest::Insert(insert_empty);
267        assert_eq!(req_empty.trace_chain().len(), 0);
268        assert_eq!(req_empty.comment(), None);
269    }
270}