Skip to main content

teaql_runtime/
lib.rs

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