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