Skip to main content

teaql_runtime/
lib.rs

1#![allow(warnings)]
2mod checker;
3mod context;
4mod data_service;
5mod entity_runtime;
6pub mod entity_save;
7mod entity_status;
8mod error;
9mod event;
10pub mod generated_support;
11mod graph;
12mod id;
13pub mod inmemory_engine;
14mod language;
15pub mod log_formatter;
16mod memory;
17mod registry;
18
19pub use context::{
20    DataStore, InMemoryDataStore, InfoLogEntry, LogPayload, SchemaProvider, SqlLogEntry,
21    SqlLogOperation, SqlLogOptions, UnifiedLogBuffer, UnifiedLogEntry, UserContext,
22};
23pub use data_service::{
24    AggregationCacheBackend, EntityDataService, GraphTransactionBoundary, InMemoryAggregationCache,
25    RelationLoadPlan,
26};
27pub use entity_runtime::{
28    ChangeSetStack, EntityChangeSet, EntityKey, EntityRoot, LedgerEntity, RootContext,
29};
30pub use entity_save::{AuditedSaveExt, graph_node_from_entity, save_audited_ledger_entity};
31pub use entity_status::{EntityAction, EntityStatus};
32pub use error::{ContextError, DataServiceError, RuntimeError};
33pub use event::{
34    EntityPropertyChange, InMemoryRawAuditEventSink, RawAuditEvent, RawAuditEventKind,
35    RawAuditEventSink, SafeAuditEvent, SafeAuditEventSink, SafeAuditField,
36};
37pub use generated_support::*;
38pub use graph::{
39    GraphMutationBatch, GraphMutationKind, GraphMutationPlan, GraphMutationPlanItem, GraphNode,
40    GraphOperation, ScopedCommentNode, TraceScopeToken, sorted_update_fields,
41};
42pub(crate) use id::local_id_generator;
43pub use id::{AtomicCounterIdGenerator, InternalIdGenerator, SnowflakeIdGenerator};
44pub use inmemory_engine::{ExprEvaluator, InMemoryQueryEngine};
45pub use language::{
46    BuiltinTranslator, Language, MessageTranslator, translate_check_result, translate_location,
47};
48pub(crate) use memory::MemoryDataService;
49pub use registry::{
50    EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityRegistry,
51    InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityRegistry, InMemoryMetadataStore,
52    MetadataStore, RequestPolicy, RuntimeModule,
53};
54
55#[cfg(test)]
56mod tests {
57    use std::collections::{BTreeMap, VecDeque};
58    use std::sync::{Arc, Mutex};
59
60    use super::{
61        AggregationCacheBackend, CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult,
62        CheckResults, CheckRule, Checker, DataServiceError, EntityDataServiceBehavior,
63        GraphMutationKind, GraphNode, InMemoryAggregationCache, InMemoryCheckerRegistry,
64        InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityRegistry, InMemoryMetadataStore,
65        InternalIdGenerator, Language, MemoryDataService, MetadataStore, ObjectLocation,
66        RawAuditEvent, RawAuditEventKind, RawAuditEventSink, RequestPolicy, RuntimeError,
67        RuntimeModule, SafeAuditEvent, SafeAuditEventSink, SqlLogOperation, SqlLogOptions,
68        TypedChecker, TypedEntityChecker, UserContext, translate_check_result,
69    };
70    use crate::data_service::RuntimeDataService;
71    use teaql_core::{
72        Aggregate, AggregateFunction, BinaryOp, DataType, Decimal, DeleteCommand, Entity,
73        EntityDescriptor, EntityError, Expr, InsertCommand, OrderBy, PropertyDescriptor, Record,
74        RecoverCommand, RelationAggregate, SelectQuery, TeaqlEntity, UpdateCommand, Value,
75    };
76    use teaql_data_service::{
77        DataServiceCapabilities, DataServiceExecutor, DataServiceOperation, ExecutionMetadata,
78        MutationExecutor, MutationRequest, MutationResult, QueryExecutor, QueryRequest,
79        QueryResult,
80    };
81    use teaql_macros::TeaqlEntity as DeriveTeaqlEntity;
82    use teaql_sql::{
83        CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, quote_identifier_if_needed,
84    };
85
86    const ORDER_DEFAULT_PROJECTION: &str = "id, version, name";
87
88    #[derive(Debug, Default, Clone, Copy)]
89    struct PostgresDialect;
90
91    impl SqlDialect for PostgresDialect {
92        fn kind(&self) -> DatabaseKind {
93            DatabaseKind::PostgreSql
94        }
95
96        fn quote_ident(&self, ident: &str) -> String {
97            quote_identifier_if_needed(ident, '"')
98        }
99
100        fn placeholder(&self, index: usize) -> String {
101            format!("${index}")
102        }
103
104        fn schema_type_sql(
105            &self,
106            data_type: DataType,
107            _property: &PropertyDescriptor,
108        ) -> Result<&'static str, SqlCompileError> {
109            match data_type {
110                DataType::Bool => Ok("BOOLEAN"),
111                DataType::I64 | DataType::U64 => Ok("BIGINT"),
112                DataType::F64 => Ok("DOUBLE PRECISION"),
113                DataType::Decimal => Ok("NUMERIC"),
114                DataType::Text => Ok("VARCHAR(255)"),
115                DataType::LargeText => Ok("TEXT"),
116                DataType::Json => Ok("JSONB"),
117                DataType::Date => Ok("DATE"),
118                DataType::Timestamp => Ok("TIMESTAMPTZ"),
119            }
120        }
121    }
122
123    fn entity() -> EntityDescriptor {
124        EntityDescriptor::new("Order")
125            .table_name("orders")
126            .property(
127                PropertyDescriptor::new("id", DataType::U64)
128                    .column_name("id")
129                    .id()
130                    .not_null(),
131            )
132            .property(
133                PropertyDescriptor::new("version", DataType::I64)
134                    .column_name("version")
135                    .version()
136                    .not_null(),
137            )
138            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
139            .relation(
140                teaql_core::RelationDescriptor::new("lines", "OrderLine")
141                    .local_key("id")
142                    .foreign_key("order_id")
143                    .many(),
144            )
145    }
146
147    fn line_entity() -> EntityDescriptor {
148        EntityDescriptor::new("OrderLine")
149            .table_name("orderline")
150            .property(
151                PropertyDescriptor::new("id", DataType::U64)
152                    .column_name("id")
153                    .id()
154                    .not_null(),
155            )
156            .property(
157                PropertyDescriptor::new("version", DataType::I64)
158                    .column_name("version")
159                    .version(),
160            )
161            .property(
162                PropertyDescriptor::new("order_id", DataType::U64)
163                    .column_name("order_id")
164                    .not_null(),
165            )
166            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
167            .property(
168                PropertyDescriptor::new("product_id", DataType::U64)
169                    .column_name("product_id")
170                    .not_null(),
171            )
172            .relation(
173                teaql_core::RelationDescriptor::new("product", "Product")
174                    .local_key("product_id")
175                    .foreign_key("id"),
176            )
177    }
178
179    fn product_entity() -> EntityDescriptor {
180        EntityDescriptor::new("Product")
181            .table_name("product")
182            .property(
183                PropertyDescriptor::new("id", DataType::U64)
184                    .column_name("id")
185                    .id()
186                    .not_null(),
187            )
188            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
189    }
190
191    #[derive(Debug, Default)]
192    struct StubExecutor {
193        affected: u64,
194        rows: Vec<Record>,
195    }
196
197    #[derive(Debug, Default)]
198    struct QueueExecutor {
199        affected: u64,
200        rows: Mutex<VecDeque<Vec<Record>>>,
201        queries: Mutex<Vec<String>>,
202    }
203
204    #[derive(Debug, Default)]
205    struct CapturingQueryExecutor {
206        rows: Vec<Record>,
207        queries: Mutex<Vec<SelectQuery>>,
208    }
209
210    struct OrderBehavior;
211
212    #[allow(dead_code)]
213    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
214    #[teaql(entity = "CatalogProduct", table = "catalog_product")]
215    struct CatalogProductRow {
216        #[teaql(id)]
217        id: u64,
218        name: String,
219    }
220
221    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
222    #[teaql(entity = "OrderAggregate", table = "orders")]
223    struct OrderAggregateDynamic {
224        #[teaql(id)]
225        id: u64,
226        #[teaql(dynamic)]
227        dynamic: BTreeMap<String, Value>,
228    }
229
230    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
231    #[teaql(entity = "Product", table = "product")]
232    struct ProductEntityRow {
233        #[teaql(id)]
234        id: u64,
235        name: String,
236    }
237
238    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
239    #[teaql(entity = "OrderLine", table = "orderline")]
240    struct OrderLineEntityRow {
241        #[teaql(id)]
242        id: u64,
243        #[teaql(column = "order_id")]
244        order_id: u64,
245        name: String,
246        #[teaql(column = "product_id")]
247        product_id: u64,
248        #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
249        product: Option<ProductEntityRow>,
250    }
251
252    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
253    #[teaql(entity = "OrderLine", table = "orderline")]
254    struct ProductLineEntityRow {
255        #[teaql(id)]
256        id: u64,
257        #[teaql(column = "order_id")]
258        order_id: u64,
259        name: String,
260        #[teaql(column = "product_id")]
261        product_id: u64,
262    }
263
264    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
265    #[teaql(entity = "Product", table = "product")]
266    struct ProductWithLinesEntityRow {
267        #[teaql(id)]
268        id: u64,
269        name: String,
270        #[teaql(relation(
271            target = "OrderLine",
272            local_key = "id",
273            foreign_key = "product_id",
274            many
275        ))]
276        lines: teaql_core::SmartList<ProductLineEntityRow>,
277    }
278
279    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
280    #[teaql(entity = "OrderLine", table = "orderline")]
281    struct OrderLineWithProductEntityRow {
282        #[teaql(id)]
283        id: u64,
284        #[teaql(column = "order_id")]
285        order_id: u64,
286        name: String,
287        #[teaql(column = "product_id")]
288        product_id: u64,
289        #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
290        product: Option<ProductWithLinesEntityRow>,
291    }
292
293    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
294    #[teaql(entity = "Order", table = "orders")]
295    struct OrderAggregateRow {
296        #[teaql(id)]
297        id: u64,
298        #[teaql(version)]
299        version: i64,
300        name: String,
301        #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
302        lines: teaql_core::SmartList<OrderLineEntityRow>,
303    }
304
305    #[derive(Debug, Clone, PartialEq, DeriveTeaqlEntity)]
306    #[teaql(entity = "Order", table = "orders")]
307    struct Order {
308        #[teaql(id)]
309        id: u64,
310        #[teaql(version)]
311        version: i64,
312        name: String,
313    }
314
315    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
316    #[teaql(entity = "Product", table = "product")]
317    struct TypedGraphProduct {
318        #[teaql(id)]
319        id: u64,
320        name: String,
321    }
322
323    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
324    #[teaql(entity = "OrderLine", table = "orderline")]
325    struct TypedGraphLine {
326        #[teaql(id)]
327        id: u64,
328        #[teaql(column = "order_id")]
329        order_id: Option<u64>,
330        name: String,
331        #[teaql(column = "product_id")]
332        product_id: Option<u64>,
333        #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
334        product: Option<TypedGraphProduct>,
335    }
336
337    #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
338    #[teaql(entity = "Order", table = "orders")]
339    struct TypedGraphOrder {
340        #[teaql(id)]
341        id: u64,
342        #[teaql(version)]
343        version: i64,
344        name: String,
345        #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
346        lines: teaql_core::SmartList<TypedGraphLine>,
347    }
348
349    #[derive(Debug, PartialEq, Eq)]
350    struct OrderEntity {
351        id: u64,
352        version: i64,
353        name: String,
354    }
355
356    impl teaql_core::TeaqlEntity for OrderEntity {
357        const ENTITY_NAME: &'static str = "Order";
358
359        fn entity_descriptor() -> EntityDescriptor {
360            entity()
361        }
362    }
363
364    impl Entity for OrderEntity {
365        fn from_record(record: Record) -> Result<Self, EntityError> {
366            let id = match record.get("id") {
367                Some(Value::U64(v)) => *v,
368                Some(Value::I64(v)) if *v >= 0 => *v as u64,
369                other => {
370                    return Err(EntityError::new(
371                        "Order",
372                        format!("invalid id field: {other:?}"),
373                    ));
374                }
375            };
376            let version = match record.get("version") {
377                Some(Value::I64(v)) => *v,
378                other => {
379                    return Err(EntityError::new(
380                        "Order",
381                        format!("invalid version field: {other:?}"),
382                    ));
383                }
384            };
385            let name = match record.get("name") {
386                Some(Value::Text(v)) => v.clone(),
387                other => {
388                    return Err(EntityError::new(
389                        "Order",
390                        format!("invalid name field: {other:?}"),
391                    ));
392                }
393            };
394            Ok(Self { id, version, name })
395        }
396
397        fn into_record(self) -> Record {
398            Record::from([
399                (String::from("id"), Value::U64(self.id)),
400                (String::from("version"), Value::I64(self.version)),
401                (String::from("name"), Value::Text(self.name)),
402            ])
403        }
404    }
405
406    #[derive(Debug)]
407    struct StubError;
408
409    impl std::fmt::Display for StubError {
410        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411            write!(f, "stub error")
412        }
413    }
414
415    impl std::error::Error for StubError {}
416
417    impl DataServiceExecutor for StubExecutor {
418        type Error = StubError;
419
420        fn capabilities(&self) -> DataServiceCapabilities {
421            DataServiceCapabilities::default()
422        }
423    }
424
425    impl QueryExecutor for StubExecutor {
426        async fn query(&self, _request: QueryRequest) -> Result<QueryResult, Self::Error> {
427            Ok(QueryResult {
428                rows: self.rows.clone(),
429                metadata: ExecutionMetadata {
430                    debug_query: None,
431                    backend: "stub".to_owned(),
432                    operation: DataServiceOperation::Query,
433                    started_at: std::time::SystemTime::now(),
434                    ended_at: std::time::SystemTime::now(),
435                    affected_rows: None,
436                    result_count: Some(self.rows.len()),
437                    trace_chain: Vec::new(),
438                    comment: None,
439                    backend_request_id: None,
440                },
441            })
442        }
443    }
444
445    impl MutationExecutor for StubExecutor {
446        async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
447            Ok(MutationResult {
448                affected_rows: self.affected,
449                generated_values: Record::new(),
450                metadata: ExecutionMetadata {
451                    debug_query: None,
452                    backend: "stub".to_owned(),
453                    operation: DataServiceOperation::Update,
454                    started_at: std::time::SystemTime::now(),
455                    ended_at: std::time::SystemTime::now(),
456                    affected_rows: Some(self.affected),
457                    result_count: None,
458                    trace_chain: Vec::new(),
459                    comment: None,
460                    backend_request_id: None,
461                },
462            })
463        }
464    }
465
466    impl DataServiceExecutor for CapturingQueryExecutor {
467        type Error = StubError;
468
469        fn capabilities(&self) -> DataServiceCapabilities {
470            DataServiceCapabilities::default()
471        }
472    }
473
474    impl QueryExecutor for CapturingQueryExecutor {
475        async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
476            self.queries.lock().unwrap().push(request.query);
477            Ok(QueryResult {
478                rows: self.rows.clone(),
479                metadata: ExecutionMetadata {
480                    debug_query: None,
481                    backend: "capture".to_owned(),
482                    operation: DataServiceOperation::Query,
483                    started_at: std::time::SystemTime::now(),
484                    ended_at: std::time::SystemTime::now(),
485                    affected_rows: None,
486                    result_count: Some(self.rows.len()),
487                    trace_chain: Vec::new(),
488                    comment: None,
489                    backend_request_id: None,
490                },
491            })
492        }
493    }
494
495    impl MutationExecutor for CapturingQueryExecutor {
496        async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
497            unreachable!("relation query test does not mutate")
498        }
499    }
500
501    impl DataServiceExecutor for QueueExecutor {
502        type Error = StubError;
503
504        fn capabilities(&self) -> DataServiceCapabilities {
505            DataServiceCapabilities::default()
506        }
507    }
508
509    impl QueryExecutor for QueueExecutor {
510        async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
511            let sql_approx = format!("SELECT ... FROM {} ...", request.query.entity);
512            self.queries.lock().unwrap().push(sql_approx);
513            Ok(QueryResult {
514                rows: self.rows.lock().unwrap().pop_front().unwrap_or_default(),
515                metadata: ExecutionMetadata {
516                    debug_query: None,
517                    backend: "queue".to_owned(),
518                    operation: DataServiceOperation::Query,
519                    started_at: std::time::SystemTime::now(),
520                    ended_at: std::time::SystemTime::now(),
521                    affected_rows: None,
522                    result_count: Some(0),
523                    trace_chain: Vec::new(),
524                    comment: None,
525                    backend_request_id: None,
526                },
527            })
528        }
529    }
530
531    impl MutationExecutor for QueueExecutor {
532        async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
533            Ok(MutationResult {
534                affected_rows: self.affected,
535                generated_values: Record::new(),
536                metadata: ExecutionMetadata {
537                    debug_query: None,
538                    backend: "queue".to_owned(),
539                    operation: DataServiceOperation::Update,
540                    started_at: std::time::SystemTime::now(),
541                    ended_at: std::time::SystemTime::now(),
542                    affected_rows: Some(self.affected),
543                    result_count: None,
544                    trace_chain: Vec::new(),
545                    comment: None,
546                    backend_request_id: None,
547                },
548            })
549        }
550    }
551
552    impl EntityDataServiceBehavior for OrderBehavior {
553        fn before_select(
554            &self,
555            _ctx: &UserContext,
556            query: &mut teaql_core::SelectQuery,
557        ) -> Result<(), RuntimeError> {
558            query.filter = Some(Expr::eq("version", 1_i64));
559            Ok(())
560        }
561
562        fn before_insert(
563            &self,
564            _ctx: &UserContext,
565            command: &mut InsertCommand,
566        ) -> Result<(), RuntimeError> {
567            command
568                .values
569                .entry("version".to_owned())
570                .or_insert(Value::I64(1));
571            Ok(())
572        }
573
574        fn relation_loads(&self, _ctx: &UserContext) -> Vec<String> {
575            vec!["lines".to_owned()]
576        }
577    }
578
579    struct ContextAwareOrderBehavior;
580    struct TenantRequestPolicy;
581    struct OrderChecker;
582    struct TypedOrderChecker;
583    #[derive(Clone)]
584    struct RecordingEventSink {
585        events: Arc<Mutex<Vec<RawAuditEvent>>>,
586    }
587    #[derive(Clone)]
588    struct RecordingSafeEventSink {
589        events: Arc<Mutex<Vec<SafeAuditEvent>>>,
590    }
591
592    impl EntityDataServiceBehavior for ContextAwareOrderBehavior {
593        fn before_insert(
594            &self,
595            ctx: &UserContext,
596            command: &mut InsertCommand,
597        ) -> Result<(), RuntimeError> {
598            let tenant = ctx
599                .get_named_resource::<String>("tenant")
600                .cloned()
601                .ok_or_else(|| RuntimeError::Behavior("missing tenant resource".to_owned()))?;
602            let version = *ctx
603                .get_named_resource::<i64>("initial_version")
604                .ok_or_else(|| {
605                    RuntimeError::Behavior("missing initial_version resource".to_owned())
606                })?;
607            let trace_id = match ctx.local("trace_id") {
608                Some(Value::Text(value)) => value.clone(),
609                other => {
610                    return Err(RuntimeError::Behavior(format!(
611                        "missing trace_id local, got {other:?}"
612                    )));
613                }
614            };
615
616            command
617                .values
618                .entry("name".to_owned())
619                .or_insert(Value::Text(format!("{tenant}:{trace_id}")));
620            command
621                .values
622                .entry("version".to_owned())
623                .or_insert(Value::I64(version));
624            Ok(())
625        }
626    }
627
628    impl RequestPolicy for TenantRequestPolicy {
629        fn enforce_select(
630            &self,
631            ctx: &UserContext,
632            query: &mut SelectQuery,
633        ) -> Result<(), RuntimeError> {
634            if query.entity == "Order" {
635                let tenant_id = ctx
636                    .get_named_resource::<u64>("tenant_id")
637                    .copied()
638                    .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
639                query.filter = Some(match query.filter.take() {
640                    Some(filter) => filter.and_expr(Expr::eq("id", tenant_id)),
641                    None => Expr::eq("id", tenant_id),
642                });
643            }
644            Ok(())
645        }
646
647        fn enforce_insert(
648            &self,
649            ctx: &UserContext,
650            command: &mut InsertCommand,
651        ) -> Result<(), RuntimeError> {
652            if command.entity == "Order" {
653                let tenant_id = ctx
654                    .get_named_resource::<u64>("tenant_id")
655                    .copied()
656                    .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
657                command
658                    .values
659                    .insert("version".to_owned(), Value::I64(tenant_id as i64));
660            }
661            Ok(())
662        }
663    }
664
665    impl Checker for OrderChecker {
666        fn entity(&self) -> &str {
667            "Order"
668        }
669
670        fn check_and_fix(
671            &self,
672            _ctx: &UserContext,
673            record: &mut Record,
674            location: &ObjectLocation,
675            results: &mut CheckResults,
676        ) {
677            let status = CheckObjectStatus::from_record(record);
678            if status.is_create() {
679                self.required(record, "name", location, results);
680                record.entry("version".to_owned()).or_insert(Value::I64(1));
681            }
682            if status.is_update()
683                && record.get("name") == Some(&Value::Text("graph-update".to_owned()))
684            {
685                record.insert(
686                    "name".to_owned(),
687                    Value::Text("graph-update-checked".to_owned()),
688                );
689            }
690            self.min_string_length(record, "name", 3, location, results);
691        }
692    }
693
694    impl TypedChecker<Order> for TypedOrderChecker {
695        fn check_and_fix_typed(
696            &self,
697            _ctx: &UserContext,
698            entity: &mut Order,
699            status: CheckObjectStatus,
700            location: &ObjectLocation,
701            results: &mut CheckResults,
702        ) {
703            if status.is_create() {
704                if entity.name.is_empty() {
705                    results.push(CheckResult::required(location.clone().member("name")));
706                }
707            }
708            if entity.name.chars().count() < 3 {
709                results.push(CheckResult::min_str(
710                    location.clone().member("name"),
711                    3,
712                    entity.name.clone(),
713                ));
714            }
715            if entity.name == "fix" {
716                entity.name = "fixed".to_owned();
717            }
718        }
719    }
720
721    impl RawAuditEventSink for RecordingEventSink {
722        fn on_event(&self, _ctx: &UserContext, event: &RawAuditEvent) -> Result<(), RuntimeError> {
723            self.events.lock().unwrap().push(event.clone());
724            Ok(())
725        }
726    }
727
728    impl SafeAuditEventSink for RecordingSafeEventSink {
729        fn on_safe_event(
730            &self,
731            _ctx: &UserContext,
732            event: &SafeAuditEvent,
733        ) -> Result<(), RuntimeError> {
734            self.events.lock().unwrap().push(event.clone());
735            Ok(())
736        }
737    }
738
739    struct FixedIdGenerator(u64);
740
741    impl InternalIdGenerator for FixedIdGenerator {
742        fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
743            Ok(self.0)
744        }
745    }
746
747    struct SequentialIdGenerator {
748        next: Mutex<u64>,
749    }
750
751    impl SequentialIdGenerator {
752        fn new(next: u64) -> Self {
753            Self {
754                next: Mutex::new(next),
755            }
756        }
757    }
758
759    impl InternalIdGenerator for SequentialIdGenerator {
760        fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
761            let mut next = self
762                .next
763                .lock()
764                .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?;
765            let id = *next;
766            *next += 1;
767            Ok(id)
768        }
769    }
770
771    #[tokio::test]
772    async fn metadata_store_registers_entities() {
773        let store = InMemoryMetadataStore::new().with_entity(entity());
774        assert!(store.entity("Order").is_some());
775    }
776
777    #[tokio::test]
778    async fn runtime_module_registers_descriptor_into_context() {
779        let ctx = UserContext::new().with_module(RuntimeModule::new().descriptor(entity()));
780        assert!(ctx.entity("Order").is_some());
781        assert!(ctx.has_entity_data_service("Order"));
782    }
783
784    #[tokio::test]
785    async fn runtime_module_registers_derived_entity_and_behavior() {
786        let ctx = UserContext::new().with_module(
787            RuntimeModule::new().entity_with_behavior::<CatalogProductRow, _>(OrderBehavior),
788        );
789        assert!(ctx.entity("CatalogProduct").is_some());
790        assert!(ctx.has_entity_data_service("CatalogProduct"));
791        assert!(ctx.entity_data_service_behavior("CatalogProduct").is_some());
792    }
793
794    #[tokio::test]
795    async fn module_macro_registers_multiple_entities() {
796        let ctx = UserContext::new().with_module(crate::module!(CatalogProductRow));
797        assert!(ctx.entity("CatalogProduct").is_some());
798        assert!(ctx.has_entity_data_service("CatalogProduct"));
799    }
800
801    #[tokio::test]
802    async fn module_macro_registers_entity_behavior_pairs() {
803        let ctx =
804            UserContext::new().with_module(crate::module!(CatalogProductRow => OrderBehavior));
805        assert!(ctx.entity("CatalogProduct").is_some());
806        assert!(ctx.entity_data_service_behavior("CatalogProduct").is_some());
807    }
808
809    #[tokio::test]
810    async fn data_service_returns_optimistic_lock_conflict() {
811        let store = InMemoryMetadataStore::new().with_entity(entity());
812        let executor = StubExecutor {
813            affected: 0,
814            rows: Vec::new(),
815        };
816        let repo = RuntimeDataService::new(&store, &executor);
817
818        let err = repo
819            .update(
820                &UpdateCommand::new("Order", 1_u64)
821                    .expected_version(3)
822                    .value("name", "next"),
823            )
824            .await
825            .unwrap_err();
826
827        match err {
828            DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. }) => {}
829            other => panic!("unexpected error: {other}"),
830        }
831    }
832
833    #[tokio::test]
834    async fn user_context_indexes_resources_and_locals() {
835        let mut ctx =
836            UserContext::new().with_metadata(InMemoryMetadataStore::new().with_entity(entity()));
837        ctx.insert_resource::<u64>(42);
838        ctx.insert_named_resource("tenant", String::from("acme"));
839        ctx.put_local("trace_id", "req-1");
840
841        assert!(ctx.entity("Order").is_some());
842        assert_eq!(ctx.get_resource::<u64>(), Some(&42));
843        assert_eq!(
844            ctx.get_named_resource::<String>("tenant"),
845            Some(&String::from("acme"))
846        );
847        assert_eq!(
848            ctx.local("trace_id"),
849            Some(&Value::Text("req-1".to_owned()))
850        );
851    }
852
853    #[tokio::test]
854    async fn user_context_builds_context_data_service() {
855        let mut ctx =
856            UserContext::new().with_metadata(InMemoryMetadataStore::new().with_entity(entity()));
857        ctx.insert_resource(PostgresDialect);
858        ctx.insert_resource(StubExecutor {
859            affected: 1,
860            rows: Vec::new(),
861        });
862
863        let repo = ctx.data_service_internal::<StubExecutor>().unwrap();
864        let affected = repo
865            .update(
866                &UpdateCommand::new("Order", 1_u64)
867                    .expected_version(3)
868                    .value("name", "next"),
869            )
870            .await
871            .unwrap();
872
873        assert_eq!(affected, 1);
874    }
875
876    #[tokio::test]
877    async fn user_context_resolves_entity_data_service_by_entity_type() {
878        let mut ctx = UserContext::new()
879            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
880            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
881        ctx.insert_resource(PostgresDialect);
882        ctx.insert_resource(StubExecutor {
883            affected: 1,
884            rows: Vec::new(),
885        });
886
887        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
888        assert_eq!(repo.entity(), "Order");
889        assert_eq!(repo.select().entity, "Order");
890
891        let affected = repo
892            .insert_internal(
893                &repo
894                    .insert_command()
895                    .value("id", 1_u64)
896                    .value("version", 1_i64)
897                    .value("name", "n"),
898            )
899            .await
900            .unwrap();
901        assert_eq!(affected, 1);
902    }
903
904    #[tokio::test]
905    async fn entity_data_service_applies_behavior_hooks() {
906        let mut ctx = UserContext::new()
907            .with_metadata(
908                InMemoryMetadataStore::new()
909                    .with_entity(entity())
910                    .with_entity(line_entity())
911                    .with_entity(product_entity()),
912            )
913            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
914            .with_entity_data_service_behavior_registry(
915                InMemoryEntityDataServiceBehaviorRegistry::new()
916                    .with_behavior("Order", OrderBehavior),
917            );
918        ctx.insert_resource(PostgresDialect);
919        ctx.insert_resource(StubExecutor {
920            affected: 1,
921            rows: Vec::new(),
922        });
923
924        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
925
926        // let compiled = repo.compile(&repo.select()).unwrap();
927        // assert!(compiled.sql.contains("WHERE (version = $1)"));
928
929        let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
930        let affected = repo.insert_internal(&insert).await.unwrap();
931        assert_eq!(affected, 1);
932        assert_eq!(repo.relation_loads(), vec!["lines".to_owned()]);
933    }
934
935    #[tokio::test]
936    async fn entity_data_service_applies_request_policy_after_behavior_hooks() {
937        let mut ctx = UserContext::new()
938            .with_metadata(
939                InMemoryMetadataStore::new()
940                    .with_entity(entity())
941                    .with_entity(line_entity())
942                    .with_entity(product_entity()),
943            )
944            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
945            .with_entity_data_service_behavior_registry(
946                InMemoryEntityDataServiceBehaviorRegistry::new()
947                    .with_behavior("Order", OrderBehavior),
948            )
949            .with_request_policy(TenantRequestPolicy);
950        ctx.insert_named_resource("tenant_id", 9_u64);
951        ctx.insert_resource(PostgresDialect);
952        ctx.insert_resource(StubExecutor {
953            affected: 1,
954            rows: Vec::new(),
955        });
956
957        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
958
959        // let compiled = repo.compile(&repo.select()).unwrap();
960        // assert!(compiled.sql.contains("version = $1"));
961        // assert!(compiled.sql.contains("id = $2"));
962
963        let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
964        let command = repo.prepare_insert_command(&insert).unwrap();
965        assert_eq!(command.values.get("version"), Some(&Value::I64(9)));
966    }
967
968    #[tokio::test]
969    async fn entity_data_service_prepares_insert_command_with_generated_id() {
970        let mut ctx = UserContext::new()
971            .with_metadata(
972                InMemoryMetadataStore::new()
973                    .with_entity(entity())
974                    .with_entity(line_entity())
975                    .with_entity(product_entity()),
976            )
977            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
978            .with_entity_data_service_behavior_registry(
979                InMemoryEntityDataServiceBehaviorRegistry::new()
980                    .with_behavior("Order", OrderBehavior),
981            )
982            .with_internal_id_generator(FixedIdGenerator(42));
983        ctx.insert_resource(PostgresDialect);
984        ctx.insert_resource(StubExecutor {
985            affected: 1,
986            rows: Vec::new(),
987        });
988
989        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
990
991        let prepared = repo
992            .prepare_insert_command(&repo.insert_command().value("id", 0_u64).value("name", "n"))
993            .unwrap();
994
995        assert_eq!(prepared.values.get("id"), Some(&Value::U64(42)));
996        assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
997        assert_eq!(
998            prepared.values.get("name"),
999            Some(&Value::Text("n".to_owned()))
1000        );
1001
1002        let prepared_zero_version = repo
1003            .prepare_insert_command(
1004                &repo
1005                    .insert_command()
1006                    .value("id", 0_u64)
1007                    .value("version", 0_i64)
1008                    .value("name", "zero-version"),
1009            )
1010            .unwrap();
1011        assert_eq!(
1012            prepared_zero_version.values.get("version"),
1013            Some(&Value::I64(1))
1014        );
1015    }
1016
1017    #[tokio::test]
1018    async fn custom_user_context_can_drive_insert_preparation() {
1019        let mut ctx = UserContext::new()
1020            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1021            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1022            .with_entity_data_service_behavior_registry(
1023                InMemoryEntityDataServiceBehaviorRegistry::new()
1024                    .with_behavior("Order", ContextAwareOrderBehavior),
1025            )
1026            .with_internal_id_generator(FixedIdGenerator(99));
1027        ctx.insert_named_resource("tenant", String::from("acme"));
1028        ctx.insert_named_resource("initial_version", 7_i64);
1029        ctx.put_local("trace_id", "req-9");
1030        ctx.insert_resource(PostgresDialect);
1031        ctx.insert_resource(StubExecutor {
1032            affected: 1,
1033            rows: Vec::new(),
1034        });
1035
1036        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1037        let prepared = repo.prepare_insert_command(&repo.insert_command()).unwrap();
1038
1039        assert_eq!(prepared.values.get("id"), Some(&Value::U64(99)));
1040        assert_eq!(prepared.values.get("version"), Some(&Value::I64(7)));
1041        assert_eq!(
1042            prepared.values.get("name"),
1043            Some(&Value::Text("acme:req-9".to_owned()))
1044        );
1045    }
1046
1047    #[tokio::test]
1048    async fn checker_registry_validates_and_fixes_insert_commands() {
1049        let mut ctx = UserContext::new()
1050            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1051            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1052            .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1053            .with_internal_id_generator(FixedIdGenerator(77));
1054        ctx.insert_resource(PostgresDialect);
1055        ctx.insert_resource(StubExecutor {
1056            affected: 1,
1057            rows: Vec::new(),
1058        });
1059
1060        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1061        let prepared = repo
1062            .prepare_insert_command(&repo.insert_command().value("name", "valid"))
1063            .unwrap();
1064
1065        assert_eq!(prepared.values.get("id"), Some(&Value::U64(77)));
1066        assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
1067        assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1068
1069        let error = repo
1070            .prepare_insert_command(&repo.insert_command().value("name", "no"))
1071            .unwrap_err();
1072        match error {
1073            RuntimeError::Check(results) => {
1074                assert_eq!(results.len(), 1);
1075                assert_eq!(results[0].location.to_string(), "name");
1076            }
1077            other => panic!("unexpected checker error: {other:?}"),
1078        }
1079    }
1080
1081    #[tokio::test]
1082    async fn typed_checker_validates_and_fixes_derived_entities_without_record_access() {
1083        let mut ctx = UserContext::new()
1084            .with_metadata(InMemoryMetadataStore::new().with_entity(Order::entity_descriptor()))
1085            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1086            .with_checker_registry(
1087                InMemoryCheckerRegistry::new()
1088                    .with_checker(TypedEntityChecker::<Order, _>::new(TypedOrderChecker)),
1089            )
1090            .with_internal_id_generator(FixedIdGenerator(79));
1091        ctx.insert_resource(PostgresDialect);
1092        ctx.insert_resource(StubExecutor {
1093            affected: 1,
1094            rows: Vec::new(),
1095        });
1096
1097        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1098        let prepared = repo
1099            .prepare_insert_command(
1100                &repo
1101                    .insert_command()
1102                    .value("name", "fix")
1103                    .value("version", 1_i64),
1104            )
1105            .unwrap();
1106        assert_eq!(
1107            prepared.values.get("name"),
1108            Some(&Value::Text("fixed".to_owned()))
1109        );
1110        assert_eq!(prepared.values.get("id"), Some(&Value::U64(79)));
1111        assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1112
1113        let error = repo
1114            .prepare_insert_command(&repo.insert_command().value("version", 1_i64))
1115            .unwrap_err();
1116        match error {
1117            RuntimeError::Check(results) => {
1118                assert!(
1119                    results
1120                        .iter()
1121                        .any(|result| result.rule == CheckRule::Required
1122                            && result.location.to_string() == "name")
1123                );
1124            }
1125            other => panic!("unexpected typed checker error: {other:?}"),
1126        }
1127    }
1128
1129    #[tokio::test]
1130    async fn checker_registry_reports_nested_create_locations_and_fixes_records() {
1131        let ctx = UserContext::new()
1132            .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker));
1133
1134        let mut child = Record::from([
1135            (String::from("id"), Value::U64(10)),
1136            (
1137                String::from(CHECK_OBJECT_STATUS_FIELD),
1138                Value::from(CheckObjectStatus::Create),
1139            ),
1140        ]);
1141        let error = ctx
1142            .check_and_fix_record_at(
1143                "Order",
1144                &mut child,
1145                &ObjectLocation::hash_root("lines").element(0),
1146            )
1147            .unwrap_err();
1148
1149        assert_eq!(child.get("version"), Some(&Value::I64(1)));
1150        match error {
1151            RuntimeError::Check(results) => {
1152                assert_eq!(results.len(), 1);
1153                assert_eq!(results[0].rule, CheckRule::Required);
1154                assert_eq!(results[0].location.to_string(), "lines[0].name");
1155            }
1156            other => panic!("unexpected checker error: {other:?}"),
1157        }
1158
1159        child.insert("name".to_owned(), Value::Text("valid child".to_owned()));
1160        ctx.check_and_fix_record_at(
1161            "Order",
1162            &mut child,
1163            &ObjectLocation::hash_root("lines").element(0),
1164        )
1165        .unwrap();
1166    }
1167
1168    #[tokio::test]
1169    async fn built_in_language_translators_cover_fifteen_languages() {
1170        assert_eq!(Language::ALL.len(), 15);
1171        let result = super::CheckResult::required(ObjectLocation::hash_root("name"));
1172        let messages = Language::ALL
1173            .iter()
1174            .map(|language| translate_check_result(*language, &result))
1175            .collect::<Vec<_>>();
1176
1177        assert!(messages.iter().all(|message| !message.is_empty()));
1178        assert!(messages.iter().any(|message| message.contains("required")));
1179        assert!(messages.iter().any(|message| message.contains("å¿…å¡«")));
1180        assert!(
1181            messages
1182                .iter()
1183                .any(|message| message.contains("obligatoire"))
1184        );
1185        assert_eq!(Language::from_code("zh-CN"), Some(Language::Chinese));
1186        assert_eq!(
1187            Language::from_code("zh-TW"),
1188            Some(Language::TraditionalChinese)
1189        );
1190    }
1191
1192    #[tokio::test]
1193    async fn user_context_language_switch_translates_checker_errors() {
1194        let mut ctx = UserContext::new()
1195            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1196            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1197            .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1198            .with_internal_id_generator(FixedIdGenerator(77))
1199            .with_language(Language::Chinese);
1200        ctx.insert_resource(PostgresDialect);
1201        ctx.insert_resource(StubExecutor {
1202            affected: 1,
1203            rows: Vec::new(),
1204        });
1205
1206        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1207        let error = repo
1208            .prepare_insert_command(&repo.insert_command())
1209            .unwrap_err();
1210        match error {
1211            RuntimeError::Check(results) => {
1212                assert_eq!(results.len(), 1);
1213                assert!(
1214                    results[0]
1215                        .message
1216                        .as_ref()
1217                        .is_some_and(|message| message.contains("å¿…å¡«"))
1218                );
1219            }
1220            other => panic!("unexpected checker error: {other:?}"),
1221        }
1222
1223        let mut ctx = UserContext::new().with_language(Language::English);
1224        ctx.set_language_code("es").unwrap();
1225        assert_eq!(ctx.language(), Language::Spanish);
1226    }
1227
1228    #[tokio::test]
1229    async fn user_context_event_sink_receives_data_service_mutation_events() {
1230        let events = Arc::new(Mutex::new(Vec::new()));
1231        let safe_events = Arc::new(Mutex::new(Vec::new()));
1232        let mut ctx = UserContext::new()
1233            .with_metadata(
1234                InMemoryMetadataStore::new()
1235                    .with_entity(entity().audit_mask_fields(vec!["name".to_owned()])),
1236            )
1237            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1238            .with_internal_id_generator(FixedIdGenerator(88))
1239            .with_event_sink(RecordingEventSink {
1240                events: events.clone(),
1241            })
1242            .with_custom_event_sink(RecordingSafeEventSink {
1243                events: safe_events.clone(),
1244            });
1245        ctx.insert_resource(PostgresDialect);
1246        ctx.insert_resource(StubExecutor {
1247            affected: 1,
1248            rows: vec![Record::from([
1249                ("id".to_owned(), Value::U64(88)),
1250                ("version".to_owned(), Value::I64(1)),
1251                ("name".to_owned(), Value::Text("old".to_owned())),
1252            ])],
1253        });
1254
1255        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1256        repo.insert_internal(&repo.insert_command().value("name", "created"))
1257            .await
1258            .unwrap();
1259        repo.update_internal(
1260            &repo
1261                .update_command(88_u64)
1262                .expected_version(1)
1263                .value("name", "updated"),
1264        )
1265        .await
1266        .unwrap();
1267        repo.delete_internal(&repo.delete_command(88_u64).expected_version(2))
1268            .await
1269            .unwrap();
1270        repo.recover_internal(&repo.recover_command(88_u64, -3))
1271            .await
1272            .unwrap();
1273
1274        let events = events.lock().unwrap();
1275        assert_eq!(events.len(), 4);
1276        assert_eq!(events[0].kind, RawAuditEventKind::Created);
1277        assert_eq!(events[0].entity, "Order");
1278        assert_eq!(events[0].values.get("id"), Some(&Value::U64(88)));
1279        assert_eq!(events[1].kind, RawAuditEventKind::Updated);
1280        assert_eq!(events[1].values.get("id"), Some(&Value::U64(88)));
1281        assert_eq!(events[1].values.get("version"), Some(&Value::I64(2)));
1282        assert_eq!(events[1].updated_fields, vec!["name".to_owned()]);
1283        assert_eq!(
1284            events[1]
1285                .old_values
1286                .as_ref()
1287                .and_then(|values| values.get("name")),
1288            None // We no longer fetch old_values dynamically
1289        );
1290        assert_eq!(
1291            events[1]
1292                .new_values
1293                .as_ref()
1294                .and_then(|values| values.get("name")),
1295            Some(&Value::Text("updated".to_owned()))
1296        );
1297        assert_eq!(events[1].changes.len(), 1);
1298        assert_eq!(events[1].changes[0].field, "name");
1299        assert_eq!(
1300            events[1].changes[0].old_value,
1301            None // Old value is now absent during blind updates
1302        );
1303        assert_eq!(
1304            events[1].changes[0].new_value,
1305            Some(Value::Text("updated".to_owned()))
1306        );
1307        assert_eq!(events[2].kind, RawAuditEventKind::Deleted);
1308        assert!(events[2].old_values.is_none()); // No longer fetched
1309        assert!(events[2].new_values.is_none());
1310        assert_eq!(events[3].kind, RawAuditEventKind::Recovered);
1311        assert_eq!(
1312            events[3]
1313                .old_values
1314                .as_ref()
1315                .and_then(|values| values.get("version")),
1316            None // No longer fetched
1317        );
1318        assert_eq!(
1319            events[3]
1320                .new_values
1321                .as_ref()
1322                .and_then(|values| values.get("version")),
1323            Some(&Value::I64(4))
1324        );
1325        assert_eq!(events[3].changes[0].field, "version");
1326        drop(events);
1327
1328        let safe_events = safe_events.lock().unwrap();
1329        assert_eq!(safe_events.len(), 4);
1330        assert_eq!(safe_events[0].kind, RawAuditEventKind::Created);
1331        let name = safe_events[0]
1332            .fields
1333            .iter()
1334            .find(|field| field.name == "name")
1335            .expect("application audit event should contain the changed name field");
1336        assert!(name.masked);
1337        assert_ne!(name.value.as_deref(), Some("created"));
1338    }
1339
1340    #[tokio::test]
1341    async fn entity_data_service_builds_relation_plans() {
1342        let mut ctx = UserContext::new()
1343            .with_metadata(
1344                InMemoryMetadataStore::new()
1345                    .with_entity(entity())
1346                    .with_entity(line_entity())
1347                    .with_entity(product_entity()),
1348            )
1349            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1350            .with_entity_data_service_behavior_registry(
1351                InMemoryEntityDataServiceBehaviorRegistry::new()
1352                    .with_behavior("Order", OrderBehavior),
1353            );
1354        ctx.insert_resource(PostgresDialect);
1355        ctx.insert_resource(StubExecutor {
1356            affected: 1,
1357            rows: Vec::new(),
1358        });
1359
1360        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1361        let plans = repo.relation_plans().unwrap();
1362
1363        assert_eq!(plans.len(), 1);
1364        assert_eq!(plans[0].relation_name, "lines");
1365        assert_eq!(plans[0].target_entity, "OrderLine");
1366        assert_eq!(plans[0].local_key, "id");
1367        assert_eq!(plans[0].foreign_key, "order_id");
1368        assert!(plans[0].many);
1369    }
1370
1371    #[tokio::test]
1372    async fn entity_data_service_builds_relation_query_from_parent_rows() {
1373        let mut ctx = UserContext::new()
1374            .with_metadata(
1375                InMemoryMetadataStore::new()
1376                    .with_entity(entity())
1377                    .with_entity(line_entity())
1378                    .with_entity(product_entity()),
1379            )
1380            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1381            .with_entity_data_service_behavior_registry(
1382                InMemoryEntityDataServiceBehaviorRegistry::new()
1383                    .with_behavior("Order", OrderBehavior),
1384            );
1385        ctx.insert_resource(PostgresDialect);
1386        ctx.insert_resource(StubExecutor {
1387            affected: 1,
1388            rows: Vec::new(),
1389        });
1390
1391        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1392        let parent_rows = vec![
1393            Record::from([(String::from("id"), Value::U64(11))]),
1394            Record::from([(String::from("id"), Value::U64(12))]),
1395        ];
1396
1397        let query = repo.relation_query("lines", &parent_rows).unwrap();
1398        // let compiled = repo.compile(&query).unwrap();
1399        // assert!(compiled.sql.contains("FROM orderline"));
1400        // assert!(compiled.sql.contains("order_id IN ($1, $2)"));
1401        // assert_eq!(compiled.params, vec![Value::U64(11), Value::U64(12)]);
1402    }
1403
1404    #[tokio::test]
1405    async fn entity_data_service_enhances_parent_rows_with_relations() {
1406        let mut ctx = UserContext::new()
1407            .with_metadata(
1408                InMemoryMetadataStore::new()
1409                    .with_entity(entity())
1410                    .with_entity(line_entity())
1411                    .with_entity(product_entity()),
1412            )
1413            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1414            .with_entity_data_service_behavior_registry(
1415                InMemoryEntityDataServiceBehaviorRegistry::new()
1416                    .with_behavior("Order", OrderBehavior),
1417            );
1418        ctx.insert_resource(PostgresDialect);
1419        ctx.insert_resource(StubExecutor {
1420            affected: 1,
1421            rows: vec![
1422                Record::from([
1423                    (String::from("id"), Value::U64(101)),
1424                    (String::from("order_id"), Value::U64(11)),
1425                    (String::from("name"), Value::Text(String::from("l1"))),
1426                ]),
1427                Record::from([
1428                    (String::from("id"), Value::U64(102)),
1429                    (String::from("order_id"), Value::U64(11)),
1430                    (String::from("name"), Value::Text(String::from("l2"))),
1431                ]),
1432                Record::from([
1433                    (String::from("id"), Value::U64(201)),
1434                    (String::from("order_id"), Value::U64(12)),
1435                    (String::from("name"), Value::Text(String::from("l3"))),
1436                ]),
1437            ],
1438        });
1439
1440        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1441        let mut parents = vec![
1442            Record::from([(String::from("id"), Value::U64(11))]),
1443            Record::from([(String::from("id"), Value::U64(12))]),
1444        ];
1445
1446        repo.enhance_relations_internal(&mut parents).await.unwrap();
1447
1448        match parents[0].get("lines") {
1449            Some(Value::List(lines)) => assert_eq!(lines.len(), 2),
1450            other => panic!("unexpected lines payload: {other:?}"),
1451        }
1452        match parents[1].get("lines") {
1453            Some(Value::List(lines)) => assert_eq!(lines.len(), 1),
1454            other => panic!("unexpected lines payload: {other:?}"),
1455        }
1456    }
1457
1458    #[tokio::test]
1459    async fn relation_limit_is_partitioned_per_parent_and_rank_is_internal() {
1460        let mut rows = Vec::new();
1461        for (order_id, first_line_id) in [(11_u64, 101_u64), (12_u64, 201_u64)] {
1462            for rank in 1_u64..=3 {
1463                rows.push(Record::from([
1464                    (String::from("id"), Value::U64(first_line_id + rank - 1)),
1465                    (String::from("order_id"), Value::U64(order_id)),
1466                    (
1467                        String::from(teaql_core::PARTITION_RANK_PROPERTY),
1468                        Value::U64(rank),
1469                    ),
1470                ]));
1471            }
1472        }
1473
1474        let mut ctx = UserContext::new()
1475            .with_metadata(
1476                InMemoryMetadataStore::new()
1477                    .with_entity(entity())
1478                    .with_entity(line_entity()),
1479            )
1480            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1481        ctx.insert_resource(PostgresDialect);
1482        ctx.insert_resource(CapturingQueryExecutor {
1483            rows,
1484            queries: Mutex::new(Vec::new()),
1485        });
1486
1487        let repo = ctx
1488            .entity_data_service::<CapturingQueryExecutor>("Order")
1489            .unwrap();
1490        let mut parents = vec![
1491            Record::from([(String::from("id"), Value::U64(11))]),
1492            Record::from([(String::from("id"), Value::U64(12))]),
1493        ];
1494        let query = SelectQuery::new("Order").relation_query(
1495            "lines",
1496            SelectQuery::new("OrderLine")
1497                .order_by(OrderBy::desc("id"))
1498                .limit(3),
1499        );
1500
1501        repo.enhance_query_relations_internal(&mut parents, &query)
1502            .await
1503            .unwrap();
1504
1505        let captured = &ctx
1506            .get_resource::<CapturingQueryExecutor>()
1507            .unwrap()
1508            .queries
1509            .lock()
1510            .unwrap()[0];
1511        assert_eq!(captured.partition_by.as_deref(), Some("order_id"));
1512        assert_eq!(captured.slice.and_then(|slice| slice.limit), Some(3));
1513        for parent in &parents {
1514            let Some(Value::List(lines)) = parent.get("lines") else {
1515                panic!("missing relation lines")
1516            };
1517            assert_eq!(lines.len(), 3);
1518            assert!(lines.iter().all(|line| match line {
1519                Value::Object(line) => !line.contains_key(teaql_core::PARTITION_RANK_PROPERTY),
1520                _ => false,
1521            }));
1522        }
1523    }
1524
1525    #[tokio::test]
1526    async fn relation_enhancement_wraps_inverse_many_relation_as_list() {
1527        let mut ctx = UserContext::new()
1528            .with_metadata(
1529                InMemoryMetadataStore::new()
1530                    .with_entity(OrderLineWithProductEntityRow::entity_descriptor())
1531                    .with_entity(ProductWithLinesEntityRow::entity_descriptor()),
1532            )
1533            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderLine"));
1534        ctx.insert_resource(PostgresDialect);
1535        ctx.insert_resource(QueueExecutor {
1536            affected: 1,
1537            rows: Mutex::new(VecDeque::from([
1538                vec![Record::from([
1539                    (String::from("id"), Value::U64(11)),
1540                    (String::from("order_id"), Value::U64(7)),
1541                    (String::from("name"), Value::Text(String::from("line"))),
1542                    (String::from("product_id"), Value::U64(101)),
1543                ])],
1544                vec![Record::from([
1545                    (String::from("id"), Value::U64(101)),
1546                    (String::from("name"), Value::Text(String::from("sku"))),
1547                ])],
1548            ])),
1549            queries: Mutex::new(Vec::new()),
1550        });
1551
1552        let repo = ctx
1553            .entity_data_service::<QueueExecutor>("OrderLine")
1554            .unwrap();
1555        let rows = repo
1556            .fetch_enhanced_entities_internal::<OrderLineWithProductEntityRow>(
1557                &SelectQuery::new("OrderLine").relation("product"),
1558            )
1559            .await
1560            .unwrap();
1561
1562        let product = rows.data[0].product.as_ref().unwrap();
1563        assert_eq!(product.lines.data.len(), 1);
1564        assert_eq!(product.lines.data[0].id, 11);
1565    }
1566
1567    #[tokio::test]
1568    async fn entity_data_service_fetches_smart_list_of_entities() {
1569        let mut ctx = UserContext::new()
1570            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1571            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1572        ctx.insert_resource(PostgresDialect);
1573        ctx.insert_resource(StubExecutor {
1574            affected: 1,
1575            rows: vec![Record::from([
1576                (String::from("id"), Value::U64(7)),
1577                (String::from("version"), Value::I64(2)),
1578                (String::from("name"), Value::Text(String::from("typed"))),
1579            ])],
1580        });
1581
1582        let repo = ctx.entity_data_service::<StubExecutor>("Order").unwrap();
1583        let rows = repo
1584            .fetch_entities_internal::<OrderEntity>(&repo.select())
1585            .await
1586            .unwrap();
1587
1588        assert_eq!(rows.len(), 1);
1589        assert_eq!(
1590            rows.first(),
1591            Some(&OrderEntity {
1592                id: 7,
1593                version: 2,
1594                name: String::from("typed"),
1595            })
1596        );
1597    }
1598
1599    #[tokio::test]
1600    async fn entity_data_service_fetches_smart_list_of_derived_entities() {
1601        let mut ctx = UserContext::new()
1602            .with_metadata(
1603                InMemoryMetadataStore::new().with_entity(CatalogProductRow::entity_descriptor()),
1604            )
1605            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("CatalogProduct"));
1606        ctx.insert_resource(PostgresDialect);
1607        ctx.insert_resource(StubExecutor {
1608            affected: 1,
1609            rows: vec![Record::from([
1610                (String::from("id"), Value::U64(9)),
1611                (String::from("name"), Value::Text(String::from("derived"))),
1612            ])],
1613        });
1614
1615        let repo = ctx
1616            .entity_data_service::<StubExecutor>("CatalogProduct")
1617            .unwrap();
1618        let rows = repo
1619            .fetch_entities_internal::<CatalogProductRow>(&repo.select())
1620            .await
1621            .unwrap();
1622
1623        assert_eq!(rows.len(), 1);
1624        assert_eq!(
1625            rows.first(),
1626            Some(&CatalogProductRow {
1627                id: 9,
1628                name: String::from("derived"),
1629            })
1630        );
1631    }
1632
1633    #[tokio::test]
1634    async fn entity_data_service_collects_dynamic_properties_for_aggregate_output() {
1635        let mut ctx = UserContext::new()
1636            .with_metadata(
1637                InMemoryMetadataStore::new()
1638                    .with_entity(OrderAggregateDynamic::entity_descriptor()),
1639            )
1640            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderAggregate"));
1641        ctx.insert_resource(PostgresDialect);
1642        ctx.insert_resource(StubExecutor {
1643            affected: 1,
1644            rows: vec![Record::from([
1645                (String::from("id"), Value::U64(1)),
1646                (String::from("lineCount"), Value::I64(3)),
1647                (String::from("amount"), Value::F64(18.5)),
1648            ])],
1649        });
1650
1651        let repo = ctx
1652            .entity_data_service::<StubExecutor>("OrderAggregate")
1653            .unwrap();
1654        let rows = repo
1655            .fetch_entities_internal::<OrderAggregateDynamic>(&repo.select())
1656            .await
1657            .unwrap();
1658
1659        assert_eq!(rows.len(), 1);
1660        assert_eq!(rows.data[0].id, 1);
1661        assert_eq!(rows.data[0].dynamic.get("lineCount"), Some(&Value::I64(3)));
1662        assert_eq!(rows.data[0].dynamic.get("amount"), Some(&Value::F64(18.5)));
1663        assert_eq!(
1664            rows.into_vec().into_iter().next().unwrap().into_json(),
1665            serde_json::json!({
1666                "id": 1,
1667                "lineCount": 3,
1668                "amount": 18.5
1669            })
1670        );
1671    }
1672
1673    #[tokio::test]
1674    async fn entity_data_service_executes_relation_aggregates_into_dynamic_properties() {
1675        let executor = QueueExecutor {
1676            affected: 1,
1677            rows: Mutex::new(VecDeque::from([
1678                vec![
1679                    Record::from([
1680                        (String::from("id"), Value::U64(1)),
1681                        (String::from("version"), Value::I64(1)),
1682                        (String::from("name"), Value::Text(String::from("first"))),
1683                    ]),
1684                    Record::from([
1685                        (String::from("id"), Value::U64(2)),
1686                        (String::from("version"), Value::I64(1)),
1687                        (String::from("name"), Value::Text(String::from("second"))),
1688                    ]),
1689                ],
1690                vec![Record::from([
1691                    (String::from("order_id"), Value::U64(1)),
1692                    (String::from("lineCount"), Value::I64(3)),
1693                ])],
1694            ])),
1695            queries: Mutex::new(Vec::new()),
1696        };
1697        let mut ctx = UserContext::new()
1698            .with_metadata(
1699                InMemoryMetadataStore::new()
1700                    .with_entity(entity())
1701                    .with_entity(line_entity()),
1702            )
1703            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1704        ctx.insert_resource(PostgresDialect);
1705        ctx.insert_resource(executor);
1706
1707        let repo = ctx.entity_data_service::<QueueExecutor>("Order").unwrap();
1708        let rows = repo
1709            .fetch_all_with_relation_aggregates_internal(
1710                &repo
1711                    .select()
1712                    .project("id")
1713                    .project("version")
1714                    .project("name"),
1715                &[RelationAggregate::new(
1716                    "lines",
1717                    "lineCount",
1718                    SelectQuery::new("OrderLine"),
1719                    true,
1720                )],
1721            )
1722            .await
1723            .unwrap();
1724
1725        assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
1726        assert_eq!(rows[1].get("lineCount"), Some(&Value::U64(0)));
1727
1728        let executor = ctx.get_resource::<QueueExecutor>().unwrap();
1729        let queries = executor.queries.lock().unwrap();
1730        assert_eq!(queries.len(), 2);
1731        assert_eq!(queries[1], "SELECT ... FROM OrderLine ...");
1732    }
1733
1734    #[tokio::test]
1735    async fn entity_data_service_maps_relation_aggregate_storage_key_to_property_key() {
1736        let mut line = line_entity();
1737        line.properties
1738            .iter_mut()
1739            .find(|property| property.name == "order_id")
1740            .unwrap()
1741            .column_name = "order_ref".to_owned();
1742        let executor = QueueExecutor {
1743            affected: 1,
1744            rows: Mutex::new(VecDeque::from([
1745                vec![Record::from([
1746                    (String::from("id"), Value::U64(1)),
1747                    (String::from("version"), Value::I64(1)),
1748                    (String::from("name"), Value::Text(String::from("first"))),
1749                ])],
1750                vec![Record::from([
1751                    (String::from("order_ref"), Value::I64(1)),
1752                    (String::from("lineCount"), Value::I64(3)),
1753                ])],
1754            ])),
1755            queries: Mutex::new(Vec::new()),
1756        };
1757        let mut ctx = UserContext::new()
1758            .with_metadata(
1759                InMemoryMetadataStore::new()
1760                    .with_entity(entity())
1761                    .with_entity(line),
1762            )
1763            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1764        ctx.insert_resource(PostgresDialect);
1765        ctx.insert_resource(executor);
1766
1767        let repo = ctx.entity_data_service::<QueueExecutor>("Order").unwrap();
1768        let rows = repo
1769            .fetch_all_with_relation_aggregates_internal(
1770                &repo
1771                    .select()
1772                    .project("id")
1773                    .project("version")
1774                    .project("name"),
1775                &[RelationAggregate::new(
1776                    "lines",
1777                    "lineCount",
1778                    SelectQuery::new("OrderLine"),
1779                    true,
1780                )],
1781            )
1782            .await
1783            .unwrap();
1784
1785        assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
1786        let executor = ctx.get_resource::<QueueExecutor>().unwrap();
1787        assert_eq!(
1788            executor.queries.lock().unwrap()[1],
1789            "SELECT ... FROM OrderLine ..."
1790        );
1791    }
1792
1793    #[tokio::test]
1794    async fn entity_data_service_uses_aggregation_cache_when_resource_is_registered() {
1795        let executor = QueueExecutor {
1796            affected: 1,
1797            rows: Mutex::new(VecDeque::from([vec![Record::from([(
1798                String::from("count"),
1799                Value::I64(2),
1800            )])]])),
1801            queries: Mutex::new(Vec::new()),
1802        };
1803        let mut ctx = UserContext::new()
1804            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1805            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1806        ctx.insert_resource(PostgresDialect);
1807        ctx.insert_resource(executor);
1808        ctx.insert_resource(InMemoryAggregationCache::default());
1809
1810        let repo = ctx.entity_data_service::<QueueExecutor>("Order").unwrap();
1811        let query = repo
1812            .select()
1813            .count("count")
1814            .enable_aggregation_cache_for(60_000);
1815
1816        let first = repo.fetch_all_internal(&query).await.unwrap();
1817        let second = repo.fetch_all_internal(&query).await.unwrap();
1818
1819        assert_eq!(first, second);
1820        let executor = ctx.get_resource::<QueueExecutor>().unwrap();
1821        assert_eq!(executor.queries.lock().unwrap().len(), 1);
1822    }
1823
1824    #[tokio::test]
1825    async fn aggregation_cache_is_namespaced_and_invalidated_after_write() {
1826        let executor = QueueExecutor {
1827            affected: 1,
1828            rows: Mutex::new(VecDeque::from([
1829                vec![Record::from([(String::from("count"), Value::I64(2))])],
1830                vec![Record::from([(String::from("count"), Value::I64(3))])],
1831            ])),
1832            queries: Mutex::new(Vec::new()),
1833        };
1834        let mut ctx = UserContext::new()
1835            .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1836            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1837        ctx.insert_resource(PostgresDialect);
1838        ctx.insert_resource(executor);
1839        ctx.insert_resource(
1840            Arc::new(InMemoryAggregationCache::with_namespace("tenant-a"))
1841                as Arc<dyn AggregationCacheBackend>,
1842        );
1843
1844        let repo = ctx.entity_data_service::<QueueExecutor>("Order").unwrap();
1845        let query = repo
1846            .select()
1847            .count("count")
1848            .enable_aggregation_cache_for(60_000);
1849
1850        let first = repo.fetch_all_internal(&query).await.unwrap();
1851        let cached = repo.fetch_all_internal(&query).await.unwrap();
1852        repo.insert_internal(
1853            &InsertCommand::new("Order")
1854                .value("id", 9_u64)
1855                .value("version", 1_i64)
1856                .value("name", "new"),
1857        )
1858        .await
1859        .unwrap();
1860        let refreshed = repo.fetch_all_internal(&query).await.unwrap();
1861
1862        assert_eq!(first, cached);
1863        assert_ne!(cached, refreshed);
1864        let executor = ctx.get_resource::<QueueExecutor>().unwrap();
1865        assert_eq!(executor.queries.lock().unwrap().len(), 2);
1866    }
1867
1868    #[tokio::test]
1869    async fn aggregation_cache_propagates_to_relation_aggregates() {
1870        let parent_rows = vec![
1871            Record::from([
1872                (String::from("id"), Value::U64(1)),
1873                (String::from("version"), Value::I64(1)),
1874                (String::from("name"), Value::Text(String::from("first"))),
1875            ]),
1876            Record::from([
1877                (String::from("id"), Value::U64(2)),
1878                (String::from("version"), Value::I64(1)),
1879                (String::from("name"), Value::Text(String::from("second"))),
1880            ]),
1881        ];
1882        let aggregate_rows = vec![Record::from([
1883            (String::from("order_id"), Value::U64(1)),
1884            (String::from("lineCount"), Value::I64(3)),
1885        ])];
1886        let executor = QueueExecutor {
1887            affected: 1,
1888            rows: Mutex::new(VecDeque::from([parent_rows, aggregate_rows])),
1889            queries: Mutex::new(Vec::new()),
1890        };
1891        let mut ctx = UserContext::new()
1892            .with_metadata(
1893                InMemoryMetadataStore::new()
1894                    .with_entity(entity())
1895                    .with_entity(line_entity()),
1896            )
1897            .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1898        ctx.insert_resource(PostgresDialect);
1899        ctx.insert_resource(executor);
1900        ctx.insert_resource(InMemoryAggregationCache::default());
1901
1902        let repo = ctx.entity_data_service::<QueueExecutor>("Order").unwrap();
1903        let query = repo
1904            .select()
1905            .project("id")
1906            .project("version")
1907            .project("name")
1908            .enable_aggregation_cache_for(60_000)
1909            .propagate_aggregation_cache(60_000);
1910        let aggregate =
1911            RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
1912
1913        let first = repo
1914            .fetch_all_with_relation_aggregates_internal(&query, &[aggregate.clone()])
1915            .await
1916            .unwrap();
1917        let second = repo
1918            .fetch_all_with_relation_aggregates_internal(&query, &[aggregate])
1919            .await
1920            .unwrap();
1921
1922        assert_eq!(first, second);
1923        let executor = ctx.get_resource::<QueueExecutor>().unwrap();
1924        assert_eq!(executor.queries.lock().unwrap().len(), 2);
1925    }
1926
1927    #[tokio::test]
1928    async fn memory_data_service_fetches_smart_list_entities_with_query_features() {
1929        let metadata = InMemoryMetadataStore::new().with_entity(entity());
1930        let data_service = MemoryDataService::new(metadata).with_rows(
1931            "Order",
1932            vec![
1933                Record::from([
1934                    (String::from("id"), Value::U64(1)),
1935                    (String::from("version"), Value::I64(1)),
1936                    (String::from("name"), Value::Text(String::from("alpha"))),
1937                ]),
1938                Record::from([
1939                    (String::from("id"), Value::U64(2)),
1940                    (String::from("version"), Value::I64(1)),
1941                    (String::from("name"), Value::Text(String::from("beta"))),
1942                ]),
1943                Record::from([
1944                    (String::from("id"), Value::U64(3)),
1945                    (String::from("version"), Value::I64(1)),
1946                    (String::from("name"), Value::Text(String::from("gamma"))),
1947                ]),
1948            ],
1949        );
1950
1951        let query = teaql_core::SelectQuery::new("Order")
1952            .filter(Expr::Binary {
1953                left: Box::new(Expr::column("id")),
1954                op: teaql_core::BinaryOp::Gte,
1955                right: Box::new(Expr::value(2_u64)),
1956            })
1957            .order_by(OrderBy::desc("id"))
1958            .limit(1);
1959
1960        let orders = data_service.fetch_entities::<Order>(&query).unwrap();
1961
1962        assert_eq!(orders.ids(), vec![Value::U64(3)]);
1963        assert_eq!(orders.versions(), vec![1]);
1964        assert_eq!(orders.first().unwrap().name, "gamma");
1965    }
1966
1967    #[tokio::test]
1968    async fn memory_data_service_runs_relation_aggregates() {
1969        let metadata = InMemoryMetadataStore::new()
1970            .with_entity(entity())
1971            .with_entity(line_entity());
1972
1973        let data_service = MemoryDataService::new(metadata)
1974            .with_rows(
1975                "Order",
1976                vec![
1977                    Record::from([
1978                        (String::from("id"), Value::U64(1)),
1979                        (String::from("version"), Value::I64(1)),
1980                        (String::from("name"), Value::Text(String::from("first"))),
1981                    ]),
1982                    Record::from([
1983                        (String::from("id"), Value::U64(2)),
1984                        (String::from("version"), Value::I64(1)),
1985                        (String::from("name"), Value::Text(String::from("second"))),
1986                    ]),
1987                ],
1988            )
1989            .with_rows(
1990                "OrderLine",
1991                vec![
1992                    Record::from([
1993                        (String::from("id"), Value::U64(10)),
1994                        (String::from("version"), Value::I64(1)),
1995                        (String::from("order_id"), Value::U64(1)),
1996                        (String::from("name"), Value::Text(String::from("line1"))),
1997                    ]),
1998                    Record::from([
1999                        (String::from("id"), Value::U64(11)),
2000                        (String::from("version"), Value::I64(1)),
2001                        (String::from("order_id"), Value::U64(1)),
2002                        (String::from("name"), Value::Text(String::from("line2"))),
2003                    ]),
2004                    Record::from([
2005                        (String::from("id"), Value::U64(12)),
2006                        (String::from("version"), Value::I64(1)),
2007                        (String::from("order_id"), Value::U64(2)),
2008                        (String::from("name"), Value::Text(String::from("line3"))),
2009                    ]),
2010                ],
2011            );
2012
2013        let query = SelectQuery::new("Order").project("id").project("name");
2014        let aggregate =
2015            RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
2016
2017        let rows = data_service
2018            .fetch_all_with_relation_aggregates(&query, &[aggregate])
2019            .unwrap();
2020
2021        assert_eq!(rows.len(), 2);
2022
2023        let first_order = rows
2024            .iter()
2025            .find(|r| r.get("id") == Some(&Value::U64(1)))
2026            .unwrap();
2027        assert_eq!(first_order.get("lineCount"), Some(&Value::U64(2)));
2028
2029        let second_order = rows
2030            .iter()
2031            .find(|r| r.get("id") == Some(&Value::U64(2)))
2032            .unwrap();
2033        assert_eq!(second_order.get("lineCount"), Some(&Value::U64(1)));
2034    }
2035
2036    #[tokio::test]
2037    async fn memory_data_service_runs_aggregates() {
2038        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2039        let data_service = MemoryDataService::new(metadata).with_rows(
2040            "Order",
2041            vec![
2042                Record::from([
2043                    (String::from("id"), Value::U64(1)),
2044                    (String::from("version"), Value::I64(1)),
2045                    (String::from("name"), Value::Text(String::from("alpha"))),
2046                ]),
2047                Record::from([
2048                    (String::from("id"), Value::U64(2)),
2049                    (String::from("version"), Value::I64(2)),
2050                    (String::from("name"), Value::Text(String::from("beta"))),
2051                ]),
2052            ],
2053        );
2054
2055        let query = teaql_core::SelectQuery {
2056            entity: String::from("Order"),
2057            projection: Vec::new(),
2058            expr_projection: Vec::new(),
2059            filter: None,
2060            having: None,
2061            order_by: Vec::new(),
2062            slice: None,
2063            partition_by: None,
2064            trace_chain: Vec::new(),
2065            aggregates: vec![
2066                Aggregate {
2067                    function: AggregateFunction::Count,
2068                    field: String::from("id"),
2069                    alias: String::from("count"),
2070                },
2071                Aggregate {
2072                    function: AggregateFunction::Sum,
2073                    field: String::from("version"),
2074                    alias: String::from("versionSum"),
2075                },
2076            ],
2077            group_by: Vec::new(),
2078            relations: Vec::new(),
2079            aggregation_cache: None,
2080            comment: None,
2081            raw_sql: None,
2082            raw_sql_search_criteria: Vec::new(),
2083            dynamic_properties: Vec::new(),
2084            raw_projections: Vec::new(),
2085            object_group_bys: Vec::new(),
2086            search_with_text: None,
2087            child_enhancements: Vec::new(),
2088            stream_config: None,
2089        };
2090
2091        let rows = data_service.fetch_all(&query).unwrap();
2092
2093        assert_eq!(rows.len(), 1);
2094        assert_eq!(rows[0].get("count"), Some(&Value::U64(2)));
2095        assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
2096    }
2097
2098    #[tokio::test]
2099    async fn memory_data_service_runs_grouped_aggregates_and_extended_filters() {
2100        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2101        let data_service = MemoryDataService::new(metadata).with_rows(
2102            "Order",
2103            vec![
2104                Record::from([
2105                    (String::from("id"), Value::U64(1)),
2106                    (String::from("version"), Value::I64(1)),
2107                    (String::from("name"), Value::Text(String::from("alpha"))),
2108                ]),
2109                Record::from([
2110                    (String::from("id"), Value::U64(2)),
2111                    (String::from("version"), Value::I64(2)),
2112                    (String::from("name"), Value::Text(String::from("alpha"))),
2113                ]),
2114                Record::from([
2115                    (String::from("id"), Value::U64(3)),
2116                    (String::from("version"), Value::I64(3)),
2117                    (String::from("name"), Value::Text(String::from("tmp-beta"))),
2118                ]),
2119            ],
2120        );
2121
2122        let rows = data_service
2123            .fetch_all(
2124                &teaql_core::SelectQuery::new("Order")
2125                    .filter(
2126                        Expr::between("version", 1_i64, 3_i64)
2127                            .and_expr(Expr::not_like("name", "tmp%"))
2128                            .and_expr(Expr::not_in_list("name", vec![Value::from("deleted")])),
2129                    )
2130                    .group_by("name")
2131                    .count("total")
2132                    .sum("version", "versionSum"),
2133            )
2134            .unwrap();
2135
2136        assert_eq!(rows.len(), 1);
2137        assert_eq!(
2138            rows[0].get("name"),
2139            Some(&Value::Text(String::from("alpha")))
2140        );
2141        assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
2142        assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
2143    }
2144
2145    #[tokio::test]
2146    async fn memory_data_service_runs_extended_aggregates_and_having() {
2147        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2148        let data_service = MemoryDataService::new(metadata).with_rows(
2149            "Order",
2150            vec![
2151                Record::from([
2152                    (String::from("id"), Value::U64(1)),
2153                    (String::from("version"), Value::I64(1)),
2154                    (String::from("name"), Value::Text(String::from("alpha"))),
2155                ]),
2156                Record::from([
2157                    (String::from("id"), Value::U64(2)),
2158                    (String::from("version"), Value::I64(3)),
2159                    (String::from("name"), Value::Text(String::from("alpha"))),
2160                ]),
2161                Record::from([
2162                    (String::from("id"), Value::U64(3)),
2163                    (String::from("version"), Value::I64(7)),
2164                    (String::from("name"), Value::Text(String::from("beta"))),
2165                ]),
2166            ],
2167        );
2168
2169        let rows = data_service
2170            .fetch_all(
2171                &teaql_core::SelectQuery::new("Order")
2172                    .group_by("name")
2173                    .count("total")
2174                    .stddev("version", "stddevVersion")
2175                    .var_pop("version", "varPopVersion")
2176                    .bit_or("version", "bitOrVersion")
2177                    .having(Expr::gt("total", 1_i64)),
2178            )
2179            .unwrap();
2180
2181        assert_eq!(rows.len(), 1);
2182        assert_eq!(
2183            rows[0].get("name"),
2184            Some(&Value::Text(String::from("alpha")))
2185        );
2186        assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
2187        assert_eq!(
2188            rows[0].get("stddevVersion").map(Value::to_json_value),
2189            Some(serde_json::Value::String(
2190                "1.4142135623730951454746218583".to_owned()
2191            ))
2192        );
2193        assert_eq!(
2194            rows[0].get("varPopVersion"),
2195            Some(&Value::Decimal(Decimal::ONE))
2196        );
2197        assert_eq!(rows[0].get("bitOrVersion"), Some(&Value::I64(3)));
2198    }
2199
2200    #[tokio::test]
2201    async fn memory_data_service_runs_sound_like_filter() {
2202        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2203        let data_service = MemoryDataService::new(metadata).with_rows(
2204            "Order",
2205            vec![
2206                Record::from([
2207                    (String::from("id"), Value::U64(1)),
2208                    (String::from("version"), Value::I64(1)),
2209                    (String::from("name"), Value::Text(String::from("Robert"))),
2210                ]),
2211                Record::from([
2212                    (String::from("id"), Value::U64(2)),
2213                    (String::from("version"), Value::I64(1)),
2214                    (String::from("name"), Value::Text(String::from("Rupert"))),
2215                ]),
2216                Record::from([
2217                    (String::from("id"), Value::U64(3)),
2218                    (String::from("version"), Value::I64(1)),
2219                    (String::from("name"), Value::Text(String::from("Ashcraft"))),
2220                ]),
2221            ],
2222        );
2223
2224        let rows = data_service
2225            .fetch_all(
2226                &teaql_core::SelectQuery::new("Order")
2227                    .filter(Expr::sound_like("name", "Robert"))
2228                    .order_asc("id"),
2229            )
2230            .unwrap();
2231
2232        assert_eq!(rows.len(), 2);
2233        assert_eq!(rows[0].get("name"), Some(&Value::Text("Robert".to_owned())));
2234        assert_eq!(rows[1].get("name"), Some(&Value::Text("Rupert".to_owned())));
2235    }
2236
2237    #[tokio::test]
2238    async fn memory_data_service_runs_java_style_string_match_filters() {
2239        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2240        let data_service = MemoryDataService::new(metadata).with_rows(
2241            "Order",
2242            vec![
2243                Record::from([
2244                    (String::from("id"), Value::U64(1)),
2245                    (String::from("version"), Value::I64(1)),
2246                    (String::from("name"), Value::Text(String::from("tea-order"))),
2247                ]),
2248                Record::from([
2249                    (String::from("id"), Value::U64(2)),
2250                    (String::from("version"), Value::I64(1)),
2251                    (
2252                        String::from("name"),
2253                        Value::Text(String::from("coffee-order")),
2254                    ),
2255                ]),
2256                Record::from([
2257                    (String::from("id"), Value::U64(3)),
2258                    (String::from("version"), Value::I64(1)),
2259                    (
2260                        String::from("name"),
2261                        Value::Text(String::from("tea-archived")),
2262                    ),
2263                ]),
2264            ],
2265        );
2266
2267        let rows = data_service
2268            .fetch_all(
2269                &teaql_core::SelectQuery::new("Order")
2270                    .filter(
2271                        Expr::contain("name", "tea")
2272                            .and_expr(Expr::begin_with("name", "tea"))
2273                            .and_expr(Expr::end_with("name", "order"))
2274                            .and_expr(Expr::not_contain("name", "coffee"))
2275                            .and_expr(Expr::not_begin_with("name", "archived"))
2276                            .and_expr(Expr::not_end_with("name", "draft")),
2277                    )
2278                    .order_asc("id"),
2279            )
2280            .unwrap();
2281
2282        assert_eq!(rows.len(), 1);
2283        assert_eq!(
2284            rows[0].get("name"),
2285            Some(&Value::Text("tea-order".to_owned()))
2286        );
2287    }
2288
2289    #[tokio::test]
2290    async fn memory_data_service_runs_property_to_property_filters() {
2291        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2292        let data_service = MemoryDataService::new(metadata).with_rows(
2293            "Order",
2294            vec![
2295                Record::from([
2296                    (String::from("id"), Value::U64(1)),
2297                    (String::from("version"), Value::I64(2)),
2298                    (String::from("name"), Value::Text(String::from("keep"))),
2299                ]),
2300                Record::from([
2301                    (String::from("id"), Value::U64(2)),
2302                    (String::from("version"), Value::I64(1)),
2303                    (String::from("name"), Value::Text(String::from("skip"))),
2304                ]),
2305            ],
2306        );
2307
2308        let rows = data_service
2309            .fetch_all(
2310                &teaql_core::SelectQuery::new("Order")
2311                    .filter(Expr::compare_columns("version", BinaryOp::Gte, "id"))
2312                    .order_asc("id"),
2313            )
2314            .unwrap();
2315
2316        assert_eq!(rows.len(), 1);
2317        assert_eq!(rows[0].get("name"), Some(&Value::Text("keep".to_owned())));
2318    }
2319
2320    #[tokio::test]
2321    async fn memory_data_service_supports_mutations_and_optimistic_locking() {
2322        let metadata = InMemoryMetadataStore::new().with_entity(entity());
2323        let data_service = MemoryDataService::new(metadata);
2324
2325        data_service
2326            .insert(
2327                &InsertCommand::new("Order")
2328                    .value("id", 10_u64)
2329                    .value("version", 1_i64)
2330                    .value("name", "draft"),
2331            )
2332            .unwrap();
2333        data_service
2334            .update(
2335                &UpdateCommand::new("Order", 10_u64)
2336                    .expected_version(1)
2337                    .value("name", "submitted"),
2338            )
2339            .unwrap();
2340
2341        let row = data_service
2342            .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2343            .unwrap()
2344            .pop()
2345            .unwrap();
2346        assert_eq!(
2347            row.get("name"),
2348            Some(&Value::Text(String::from("submitted")))
2349        );
2350        assert_eq!(row.get("version"), Some(&Value::I64(2)));
2351
2352        let conflict = data_service
2353            .update(
2354                &UpdateCommand::new("Order", 10_u64)
2355                    .expected_version(1)
2356                    .value("name", "stale"),
2357            )
2358            .unwrap_err();
2359        assert!(matches!(
2360            conflict,
2361            DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. })
2362        ));
2363
2364        data_service
2365            .delete(&DeleteCommand::new("Order", 10_u64).expected_version(2))
2366            .unwrap();
2367        let row = data_service
2368            .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2369            .unwrap()
2370            .pop()
2371            .unwrap();
2372        assert_eq!(row.get("version"), Some(&Value::I64(-3)));
2373
2374        data_service
2375            .recover(&RecoverCommand::new("Order", 10_u64, -3))
2376            .unwrap();
2377        let row = data_service
2378            .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2379            .unwrap()
2380            .pop()
2381            .unwrap();
2382        assert_eq!(row.get("version"), Some(&Value::I64(4)));
2383    }
2384
2385    #[tokio::test]
2386    async fn user_context_reports_missing_schema_provider() {
2387        let err = UserContext::new().ensure_schema().await.unwrap_err();
2388        assert!(
2389            matches!(err, RuntimeError::Schema(message) if message == "missing schema provider")
2390        );
2391    }
2392
2393    #[tokio::test]
2394    async fn user_context_stores_and_exposes_user_identifier() {
2395        let mut ctx = UserContext::new();
2396        let pid = std::process::id();
2397        let thread_id_str = format!("{:?}", std::thread::current().id());
2398        let numeric_thread_id = thread_id_str
2399            .strip_prefix("ThreadId(")
2400            .and_then(|s| s.strip_suffix(")"))
2401            .unwrap_or(&thread_id_str);
2402        let os_user = std::env::var("USER")
2403            .or_else(|_| std::env::var("USERNAME"))
2404            .unwrap_or_else(|_| "main".to_owned());
2405        let expected_default = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
2406        assert_eq!(ctx.user_identifier(), Some(expected_default.as_str()));
2407
2408        ctx.set_user_identifier("user-123");
2409        assert_eq!(ctx.user_identifier(), Some("user-123"));
2410
2411        let ctx2 = UserContext::new().with_user_identifier("user-456");
2412        assert_eq!(ctx2.user_identifier(), Some("user-456"));
2413
2414        let mut ctx3 = UserContext::new();
2415        ctx3.set_user_identifier_option(Some("user-789".to_owned()));
2416        assert_eq!(ctx3.user_identifier(), Some("user-789"));
2417        ctx3.set_user_identifier_option(None);
2418        assert_eq!(ctx3.user_identifier(), None);
2419
2420        let ctx4 = UserContext::new().with_user_identifier_option(Some("user-abc".to_owned()));
2421        assert_eq!(ctx4.user_identifier(), Some("user-abc"));
2422    }
2423}
2424
2425pub use checker::{
2426    CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult, CheckResults, CheckRule, Checker,
2427    CheckerRegistry, InMemoryCheckerRegistry, LocationSegment, ObjectLocation, TypedChecker,
2428    TypedEntityChecker, clear_record_status, mark_record_status,
2429};