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