Skip to main content

teaql_runtime/
lib.rs

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