Skip to main content

teaql_runtime/
lib.rs

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