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