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#[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 #[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 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 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 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 );
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 );
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()); 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 );
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 }
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};