Skip to main content

teaql_runtime/
lib.rs

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