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