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