Skip to main content

teaql_runtime/
lib.rs

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