Skip to main content

teaql_runtime/
lib.rs

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