Skip to main content

teaql_runtime/
lib.rs

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