Skip to main content

teaql_data_service/
lib.rs

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