Skip to main content

teaql_runtime/
lib.rs

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