Skip to main content

teaql_runtime/
lib.rs

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