Skip to main content

teaql_runtime/
lib.rs

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