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