1#![allow(warnings)]
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;
17mod memory;
18mod registry;
19mod telemetry;
20#[cfg(feature = "opentelemetry")]
21mod telemetry_opentelemetry;
22
23pub use context::{
24 ContextEntityRef, ContextRootError, ContinuousPageCursor, ContinuousPageCursorStore, DataStore,
25 InMemoryContinuousPageCursorStore, InMemoryDataStore, InfoLogEntry, LogPayload,
26 RemoteLockProvider, SchemaProvider, SqlLogEntry, SqlLogOperation, SqlLogOptions,
27 UnifiedLogBuffer, UnifiedLogEntry, UserContext,
28};
29pub use data_service::{
30 AggregationCacheBackend, EntityDataService, GraphTransactionBoundary, InMemoryAggregationCache,
31 RelationLoadPlan,
32};
33pub use entity_runtime::{
34 ChangeSetStack, EntityChangeSet, EntityKey, EntityRoot, LedgerEntity, RootContext,
35};
36pub use entity_save::{AuditedSaveExt, graph_node_from_entity, save_audited_ledger_entity};
37pub use entity_status::{EntityAction, EntityStatus};
38pub use error::{ContextError, DataServiceError, RuntimeError};
39pub use event::{
40 EntityPropertyChange, InMemoryRawAuditEventSink, RawAuditEvent, RawAuditEventKind,
41 RawAuditEventSink, SafeAuditEvent, SafeAuditEventSink, SafeAuditField,
42};
43pub use generated_support::*;
44pub use graph::{
45 GraphMutationBatch, GraphMutationKind, GraphMutationPlan, GraphMutationPlanItem, GraphNode,
46 GraphOperation, ScopedCommentNode, TraceScopeToken, sorted_update_fields,
47};
48pub use i18n::I18nCatalog;
49pub(crate) use id::local_id_generator;
50pub use id::{AtomicCounterIdGenerator, InternalIdGenerator, SnowflakeIdGenerator};
51pub use inmemory_engine::{ExprEvaluator, InMemoryQueryEngine};
52pub use language::{
53 BuiltinTranslator, Language, Locale, MessageTranslator, translate_check_result,
54 translate_location,
55};
56pub(crate) use memory::MemoryDataService;
57pub use registry::{
58 EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityRegistry,
59 InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityRegistry, InMemoryMetadataStore,
60 MetadataStore, RequestPolicy, RuntimeModule,
61};
62pub use telemetry::{
63 FailOpenRuntimeTelemetryPropagationContext, FailOpenRuntimeTelemetryScope,
64 NoopRuntimeTelemetry, RuntimeAttributeValue, RuntimeOperation, RuntimeTelemetry,
65 RuntimeTelemetryPropagationContext, RuntimeTelemetryScope, extract_runtime_context,
66 runtime_error_category, start_runtime_operation,
67};
68#[cfg(feature = "opentelemetry")]
69pub use telemetry_opentelemetry::OpenTelemetryRuntimeTelemetry;
70
71#[cfg(test)]
72mod tests {
73 use std::collections::{BTreeMap, VecDeque};
74 use std::sync::{Arc, Mutex};
75
76 use super::{
77 AggregationCacheBackend, CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult,
78 CheckResults, CheckRule, Checker, DataServiceError, EntityDataServiceBehavior,
79 GraphMutationKind, GraphNode, I18nCatalog, InMemoryAggregationCache,
80 InMemoryCheckerRegistry, InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityRegistry,
81 InMemoryMetadataStore, InternalIdGenerator, Language, MemoryDataService, MetadataStore,
82 ObjectLocation, RawAuditEvent, RawAuditEventKind, RawAuditEventSink, RemoteLockProvider,
83 RequestPolicy, RuntimeError, RuntimeModule, RuntimeOperation, RuntimeTelemetry,
84 RuntimeTelemetryScope, SafeAuditEvent, SafeAuditEventSink, SqlLogOperation, SqlLogOptions,
85 TypedChecker, TypedEntityChecker, UserContext, translate_check_result,
86 };
87 use crate::data_service::RuntimeDataService;
88 use teaql_core::{
89 Aggregate, AggregateFunction, BinaryOp, DataType, Decimal, DeleteCommand, Entity,
90 EntityDescriptor, EntityError, Expr, InsertCommand, OrderBy, PropertyDescriptor, Record,
91 RecoverCommand, RelationAggregate, SelectQuery, TeaqlEntity, UpdateCommand, Value,
92 };
93 use teaql_data_service::{
94 DataServiceCapabilities, DataServiceExecutor, DataServiceOperation, ExecutionMetadata,
95 MutationExecutor, MutationRequest, MutationResult, QueryExecutor, QueryRequest,
96 QueryResult,
97 };
98 use teaql_macros::TeaqlEntity as DeriveTeaqlEntity;
99 use teaql_sql::{
100 CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, quote_identifier_if_needed,
101 };
102
103 const ORDER_DEFAULT_PROJECTION: &str = "id, version, name";
104
105 #[derive(Debug, Default, Clone, Copy)]
106 struct PostgresDialect;
107
108 impl SqlDialect for PostgresDialect {
109 fn kind(&self) -> DatabaseKind {
110 DatabaseKind::PostgreSql
111 }
112
113 fn quote_ident(&self, ident: &str) -> String {
114 quote_identifier_if_needed(ident, '"')
115 }
116
117 fn placeholder(&self, index: usize) -> String {
118 format!("${index}")
119 }
120
121 fn schema_type_sql(
122 &self,
123 data_type: DataType,
124 _property: &PropertyDescriptor,
125 ) -> Result<&'static str, SqlCompileError> {
126 match data_type {
127 DataType::Bool => Ok("BOOLEAN"),
128 DataType::I64 | DataType::U64 => Ok("BIGINT"),
129 DataType::F64 => Ok("DOUBLE PRECISION"),
130 DataType::Decimal => Ok("NUMERIC"),
131 DataType::Text => Ok("VARCHAR(255)"),
132 DataType::LargeText => Ok("TEXT"),
133 DataType::Json => Ok("JSONB"),
134 DataType::Date => Ok("DATE"),
135 DataType::Timestamp => Ok("TIMESTAMPTZ"),
136 }
137 }
138 }
139
140 fn entity() -> EntityDescriptor {
141 EntityDescriptor::new("Order")
142 .table_name("orders")
143 .property(
144 PropertyDescriptor::new("id", DataType::U64)
145 .column_name("id")
146 .id()
147 .not_null(),
148 )
149 .property(
150 PropertyDescriptor::new("version", DataType::I64)
151 .column_name("version")
152 .version()
153 .not_null(),
154 )
155 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
156 .relation(
157 teaql_core::RelationDescriptor::new("lines", "OrderLine")
158 .local_key("id")
159 .foreign_key("order_id")
160 .many(),
161 )
162 }
163
164 fn line_entity() -> EntityDescriptor {
165 EntityDescriptor::new("OrderLine")
166 .table_name("orderline")
167 .property(
168 PropertyDescriptor::new("id", DataType::U64)
169 .column_name("id")
170 .id()
171 .not_null(),
172 )
173 .property(
174 PropertyDescriptor::new("version", DataType::I64)
175 .column_name("version")
176 .version(),
177 )
178 .property(
179 PropertyDescriptor::new("order_id", DataType::U64)
180 .column_name("order_id")
181 .not_null(),
182 )
183 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
184 .property(
185 PropertyDescriptor::new("product_id", DataType::U64)
186 .column_name("product_id")
187 .not_null(),
188 )
189 .relation(
190 teaql_core::RelationDescriptor::new("product", "Product")
191 .local_key("product_id")
192 .foreign_key("id"),
193 )
194 }
195
196 fn product_entity() -> EntityDescriptor {
197 EntityDescriptor::new("Product")
198 .table_name("product")
199 .property(
200 PropertyDescriptor::new("id", DataType::U64)
201 .column_name("id")
202 .id()
203 .not_null(),
204 )
205 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
206 }
207
208 #[derive(Debug, Default)]
209 struct StubExecutor {
210 affected: u64,
211 rows: Vec<Record>,
212 }
213
214 #[derive(Debug, Default)]
215 struct QueueExecutor {
216 affected: u64,
217 rows: Mutex<VecDeque<Vec<Record>>>,
218 queries: Mutex<Vec<String>>,
219 }
220
221 #[derive(Debug, Default)]
222 struct CapturingQueryExecutor {
223 rows: Vec<Record>,
224 queries: Mutex<Vec<SelectQuery>>,
225 }
226
227 struct OrderBehavior;
228
229 #[allow(dead_code)]
230 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
231 #[teaql(entity = "CatalogProduct", table = "catalog_product")]
232 struct CatalogProductRow {
233 #[teaql(id)]
234 id: u64,
235 name: String,
236 }
237
238 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
239 #[teaql(entity = "OrderAggregate", table = "orders")]
240 struct OrderAggregateDynamic {
241 #[teaql(id)]
242 id: u64,
243 #[teaql(dynamic)]
244 dynamic: BTreeMap<String, Value>,
245 }
246
247 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
248 #[teaql(entity = "Product", table = "product")]
249 struct ProductEntityRow {
250 #[teaql(id)]
251 id: u64,
252 name: String,
253 }
254
255 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
256 #[teaql(entity = "OrderLine", table = "orderline")]
257 struct OrderLineEntityRow {
258 #[teaql(id)]
259 id: u64,
260 #[teaql(column = "order_id")]
261 order_id: u64,
262 name: String,
263 #[teaql(column = "product_id")]
264 product_id: u64,
265 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
266 product: Option<ProductEntityRow>,
267 }
268
269 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
270 #[teaql(entity = "OrderLine", table = "orderline")]
271 struct ProductLineEntityRow {
272 #[teaql(id)]
273 id: u64,
274 #[teaql(column = "order_id")]
275 order_id: u64,
276 name: String,
277 #[teaql(column = "product_id")]
278 product_id: u64,
279 }
280
281 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
282 #[teaql(entity = "Product", table = "product")]
283 struct ProductWithLinesEntityRow {
284 #[teaql(id)]
285 id: u64,
286 name: String,
287 #[teaql(relation(
288 target = "OrderLine",
289 local_key = "id",
290 foreign_key = "product_id",
291 many
292 ))]
293 lines: teaql_core::SmartList<ProductLineEntityRow>,
294 }
295
296 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
297 #[teaql(entity = "OrderLine", table = "orderline")]
298 struct OrderLineWithProductEntityRow {
299 #[teaql(id)]
300 id: u64,
301 #[teaql(column = "order_id")]
302 order_id: u64,
303 name: String,
304 #[teaql(column = "product_id")]
305 product_id: u64,
306 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
307 product: Option<ProductWithLinesEntityRow>,
308 }
309
310 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
311 #[teaql(entity = "Order", table = "orders")]
312 struct OrderAggregateRow {
313 #[teaql(id)]
314 id: u64,
315 #[teaql(version)]
316 version: i64,
317 name: String,
318 #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
319 lines: teaql_core::SmartList<OrderLineEntityRow>,
320 }
321
322 #[derive(Debug, Clone, PartialEq, DeriveTeaqlEntity)]
323 #[teaql(entity = "Order", table = "orders")]
324 struct Order {
325 #[teaql(id)]
326 id: u64,
327 #[teaql(version)]
328 version: i64,
329 name: String,
330 }
331
332 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
333 #[teaql(entity = "Product", table = "product")]
334 struct TypedGraphProduct {
335 #[teaql(id)]
336 id: u64,
337 name: String,
338 }
339
340 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
341 #[teaql(entity = "OrderLine", table = "orderline")]
342 struct TypedGraphLine {
343 #[teaql(id)]
344 id: u64,
345 #[teaql(column = "order_id")]
346 order_id: Option<u64>,
347 name: String,
348 #[teaql(column = "product_id")]
349 product_id: Option<u64>,
350 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
351 product: Option<TypedGraphProduct>,
352 }
353
354 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
355 #[teaql(entity = "Order", table = "orders")]
356 struct TypedGraphOrder {
357 #[teaql(id)]
358 id: u64,
359 #[teaql(version)]
360 version: i64,
361 name: String,
362 #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
363 lines: teaql_core::SmartList<TypedGraphLine>,
364 }
365
366 #[derive(Debug, PartialEq, Eq)]
367 struct OrderEntity {
368 id: u64,
369 version: i64,
370 name: String,
371 }
372
373 impl teaql_core::TeaqlEntity for OrderEntity {
374 const ENTITY_NAME: &'static str = "Order";
375
376 fn entity_descriptor() -> EntityDescriptor {
377 entity()
378 }
379 }
380
381 impl Entity for OrderEntity {
382 fn from_record(record: Record) -> Result<Self, EntityError> {
383 let id = match record.get("id") {
384 Some(Value::U64(v)) => *v,
385 Some(Value::I64(v)) if *v >= 0 => *v as u64,
386 other => {
387 return Err(EntityError::new(
388 "Order",
389 format!("invalid id field: {other:?}"),
390 ));
391 }
392 };
393 let version = match record.get("version") {
394 Some(Value::I64(v)) => *v,
395 other => {
396 return Err(EntityError::new(
397 "Order",
398 format!("invalid version field: {other:?}"),
399 ));
400 }
401 };
402 let name = match record.get("name") {
403 Some(Value::Text(v)) => v.clone(),
404 other => {
405 return Err(EntityError::new(
406 "Order",
407 format!("invalid name field: {other:?}"),
408 ));
409 }
410 };
411 Ok(Self { id, version, name })
412 }
413
414 fn into_record(self) -> Record {
415 Record::from([
416 (String::from("id"), Value::U64(self.id)),
417 (String::from("version"), Value::I64(self.version)),
418 (String::from("name"), Value::Text(self.name)),
419 ])
420 }
421 }
422
423 #[derive(Debug)]
424 struct StubError;
425
426 struct RecordingRuntimeTelemetry(Arc<Mutex<Vec<String>>>);
427
428 impl RuntimeTelemetry for RecordingRuntimeTelemetry {
429 fn start(&self, operation: RuntimeOperation) -> Box<dyn RuntimeTelemetryScope> {
430 self.0
431 .lock()
432 .unwrap()
433 .push(format!("start:{}", operation.family));
434 Box::new(RecordingRuntimeTelemetryScope(self.0.clone()))
435 }
436 }
437
438 struct RecordingRuntimeTelemetryScope(Arc<Mutex<Vec<String>>>);
439
440 impl RuntimeTelemetryScope for RecordingRuntimeTelemetryScope {
441 fn success(&mut self, _attributes: BTreeMap<String, crate::RuntimeAttributeValue>) {
442 self.0.lock().unwrap().push("success".to_owned());
443 }
444
445 fn failure(&mut self, _error_type: &str) {
446 self.0.lock().unwrap().push("failure".to_owned());
447 }
448 }
449
450 impl std::fmt::Display for StubError {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 write!(f, "stub error")
453 }
454 }
455
456 impl std::error::Error for StubError {}
457
458 impl DataServiceExecutor for StubExecutor {
459 type Error = StubError;
460
461 fn capabilities(&self) -> DataServiceCapabilities {
462 DataServiceCapabilities::default()
463 }
464 }
465
466 impl QueryExecutor for StubExecutor {
467 async fn query(&self, _request: QueryRequest) -> Result<QueryResult, Self::Error> {
468 Ok(QueryResult {
469 rows: self.rows.clone(),
470 metadata: ExecutionMetadata {
471 debug_query: None,
472 backend: "stub".to_owned(),
473 operation: DataServiceOperation::Query,
474 started_at: std::time::SystemTime::now(),
475 ended_at: std::time::SystemTime::now(),
476 affected_rows: None,
477 result_count: Some(self.rows.len()),
478 trace_chain: Vec::new(),
479 comment: None,
480 backend_request_id: None,
481 parameterized_query: None,
482 params: Vec::new(),
483 },
484 })
485 }
486 }
487
488 impl MutationExecutor for StubExecutor {
489 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
490 Ok(MutationResult {
491 affected_rows: self.affected,
492 generated_values: Record::new(),
493 persisted_record: None,
494 metadata: ExecutionMetadata {
495 debug_query: None,
496 backend: "stub".to_owned(),
497 operation: DataServiceOperation::Update,
498 started_at: std::time::SystemTime::now(),
499 ended_at: std::time::SystemTime::now(),
500 affected_rows: Some(self.affected),
501 result_count: None,
502 trace_chain: Vec::new(),
503 comment: None,
504 backend_request_id: None,
505 parameterized_query: None,
506 params: Vec::new(),
507 },
508 })
509 }
510 }
511
512 impl DataServiceExecutor for CapturingQueryExecutor {
513 type Error = StubError;
514
515 fn capabilities(&self) -> DataServiceCapabilities {
516 DataServiceCapabilities::default()
517 }
518 }
519
520 impl QueryExecutor for CapturingQueryExecutor {
521 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
522 self.queries.lock().unwrap().push(request.query);
523 Ok(QueryResult {
524 rows: self.rows.clone(),
525 metadata: ExecutionMetadata {
526 debug_query: None,
527 backend: "capture".to_owned(),
528 operation: DataServiceOperation::Query,
529 started_at: std::time::SystemTime::now(),
530 ended_at: std::time::SystemTime::now(),
531 affected_rows: None,
532 result_count: Some(self.rows.len()),
533 trace_chain: Vec::new(),
534 comment: None,
535 backend_request_id: None,
536 parameterized_query: None,
537 params: Vec::new(),
538 },
539 })
540 }
541 }
542
543 impl MutationExecutor for CapturingQueryExecutor {
544 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
545 unreachable!("relation query test does not mutate")
546 }
547 }
548
549 impl DataServiceExecutor for QueueExecutor {
550 type Error = StubError;
551
552 fn capabilities(&self) -> DataServiceCapabilities {
553 DataServiceCapabilities::default()
554 }
555 }
556
557 impl QueryExecutor for QueueExecutor {
558 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
559 let sql_approx = format!("SELECT ... FROM {} ...", request.query.entity);
560 self.queries.lock().unwrap().push(sql_approx);
561 Ok(QueryResult {
562 rows: self.rows.lock().unwrap().pop_front().unwrap_or_default(),
563 metadata: ExecutionMetadata {
564 debug_query: None,
565 backend: "queue".to_owned(),
566 operation: DataServiceOperation::Query,
567 started_at: std::time::SystemTime::now(),
568 ended_at: std::time::SystemTime::now(),
569 affected_rows: None,
570 result_count: Some(0),
571 trace_chain: Vec::new(),
572 comment: None,
573 backend_request_id: None,
574 parameterized_query: None,
575 params: Vec::new(),
576 },
577 })
578 }
579 }
580
581 impl MutationExecutor for QueueExecutor {
582 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
583 Ok(MutationResult {
584 affected_rows: self.affected,
585 generated_values: Record::new(),
586 persisted_record: None,
587 metadata: ExecutionMetadata {
588 debug_query: None,
589 backend: "queue".to_owned(),
590 operation: DataServiceOperation::Update,
591 started_at: std::time::SystemTime::now(),
592 ended_at: std::time::SystemTime::now(),
593 affected_rows: Some(self.affected),
594 result_count: None,
595 trace_chain: Vec::new(),
596 comment: None,
597 backend_request_id: None,
598 parameterized_query: None,
599 params: Vec::new(),
600 },
601 })
602 }
603 }
604
605 impl EntityDataServiceBehavior for OrderBehavior {
606 fn before_select(
607 &self,
608 _ctx: &UserContext,
609 query: &mut teaql_core::SelectQuery,
610 ) -> Result<(), RuntimeError> {
611 query.filter = Some(Expr::eq("version", 1_i64));
612 Ok(())
613 }
614
615 fn before_insert(
616 &self,
617 _ctx: &UserContext,
618 command: &mut InsertCommand,
619 ) -> Result<(), RuntimeError> {
620 command
621 .values
622 .entry("version".to_owned())
623 .or_insert(Value::I64(1));
624 Ok(())
625 }
626
627 fn relation_loads(&self, _ctx: &UserContext) -> Vec<String> {
628 vec!["lines".to_owned()]
629 }
630 }
631
632 struct ContextAwareOrderBehavior;
633 struct TenantRequestPolicy;
634 struct OrderChecker;
635 struct TypedOrderChecker;
636 #[derive(Clone)]
637 struct RecordingEventSink {
638 events: Arc<Mutex<Vec<RawAuditEvent>>>,
639 }
640 #[derive(Clone)]
641 struct RecordingSafeEventSink {
642 events: Arc<Mutex<Vec<SafeAuditEvent>>>,
643 }
644
645 impl EntityDataServiceBehavior for ContextAwareOrderBehavior {
646 fn before_insert(
647 &self,
648 context: &UserContext,
649 command: &mut InsertCommand,
650 ) -> Result<(), RuntimeError> {
651 let tenant = context
652 .get_named_resource::<String>("tenant")
653 .cloned()
654 .ok_or_else(|| RuntimeError::Behavior("missing tenant resource".to_owned()))?;
655 let version = *context
656 .get_named_resource::<i64>("initial_version")
657 .ok_or_else(|| {
658 RuntimeError::Behavior("missing initial_version resource".to_owned())
659 })?;
660 let trace_id = match context.local("trace_id") {
661 Some(Value::Text(value)) => value.clone(),
662 other => {
663 return Err(RuntimeError::Behavior(format!(
664 "missing trace_id local, got {other:?}"
665 )));
666 }
667 };
668
669 command
670 .values
671 .entry("name".to_owned())
672 .or_insert(Value::Text(format!("{tenant}:{trace_id}")));
673 command
674 .values
675 .entry("version".to_owned())
676 .or_insert(Value::I64(version));
677 Ok(())
678 }
679 }
680
681 impl RequestPolicy for TenantRequestPolicy {
682 fn enforce_select(
683 &self,
684 context: &UserContext,
685 query: &mut SelectQuery,
686 ) -> Result<(), RuntimeError> {
687 if query.entity == "Order" {
688 let tenant_id = context
689 .get_named_resource::<u64>("tenant_id")
690 .copied()
691 .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
692 query.filter = Some(match query.filter.take() {
693 Some(filter) => filter.and_expr(Expr::eq("id", tenant_id)),
694 None => Expr::eq("id", tenant_id),
695 });
696 }
697 Ok(())
698 }
699
700 fn enforce_insert(
701 &self,
702 context: &UserContext,
703 command: &mut InsertCommand,
704 ) -> Result<(), RuntimeError> {
705 if command.entity == "Order" {
706 let tenant_id = context
707 .get_named_resource::<u64>("tenant_id")
708 .copied()
709 .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
710 command
711 .values
712 .insert("version".to_owned(), Value::I64(tenant_id as i64));
713 }
714 Ok(())
715 }
716 }
717
718 impl Checker for OrderChecker {
719 fn entity(&self) -> &str {
720 "Order"
721 }
722
723 fn check_and_fix(
724 &self,
725 _ctx: &UserContext,
726 record: &mut Record,
727 location: &ObjectLocation,
728 results: &mut CheckResults,
729 ) {
730 let status = CheckObjectStatus::from_record(record);
731 if status.is_create() {
732 self.required(record, "name", location, results);
733 record.entry("version".to_owned()).or_insert(Value::I64(1));
734 }
735 if status.is_update()
736 && record.get("name") == Some(&Value::Text("graph-update".to_owned()))
737 {
738 record.insert(
739 "name".to_owned(),
740 Value::Text("graph-update-checked".to_owned()),
741 );
742 }
743 self.min_string_length(record, "name", 3, location, results);
744 }
745 }
746
747 impl TypedChecker<Order> for TypedOrderChecker {
748 fn check_and_fix_typed(
749 &self,
750 _ctx: &UserContext,
751 entity: &mut Order,
752 status: CheckObjectStatus,
753 location: &ObjectLocation,
754 results: &mut CheckResults,
755 ) {
756 if status.is_create() {
757 if entity.name.is_empty() {
758 results.push(CheckResult::required(location.clone().member("name")));
759 }
760 }
761 if entity.name.chars().count() < 3 {
762 results.push(CheckResult::min_str(
763 location.clone().member("name"),
764 3,
765 entity.name.clone(),
766 ));
767 }
768 if entity.name == "fix" {
769 entity.name = "fixed".to_owned();
770 }
771 }
772 }
773
774 impl RawAuditEventSink for RecordingEventSink {
775 fn on_event(&self, _ctx: &UserContext, event: &RawAuditEvent) -> Result<(), RuntimeError> {
776 self.events.lock().unwrap().push(event.clone());
777 Ok(())
778 }
779 }
780
781 impl SafeAuditEventSink for RecordingSafeEventSink {
782 fn on_safe_event(
783 &self,
784 _ctx: &UserContext,
785 event: &SafeAuditEvent,
786 ) -> Result<(), RuntimeError> {
787 self.events.lock().unwrap().push(event.clone());
788 Ok(())
789 }
790 }
791
792 struct FixedIdGenerator(u64);
793
794 impl InternalIdGenerator for FixedIdGenerator {
795 fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
796 Ok(self.0)
797 }
798 }
799
800 struct SequentialIdGenerator {
801 next: Mutex<u64>,
802 }
803
804 impl SequentialIdGenerator {
805 fn new(next: u64) -> Self {
806 Self {
807 next: Mutex::new(next),
808 }
809 }
810 }
811
812 impl InternalIdGenerator for SequentialIdGenerator {
813 fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
814 let mut next = self
815 .next
816 .lock()
817 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?;
818 let id = *next;
819 *next += 1;
820 Ok(id)
821 }
822 }
823
824 #[tokio::test]
825 async fn metadata_store_registers_entities() {
826 let store = InMemoryMetadataStore::new().with_entity(entity());
827 assert!(store.entity("Order").is_some());
828 }
829
830 #[tokio::test]
831 async fn runtime_module_registers_descriptor_into_context() {
832 let context = UserContext::new().with_module(RuntimeModule::new().descriptor(entity()));
833 assert!(context.entity("Order").is_some());
834 assert!(context.has_entity_data_service("Order"));
835 }
836
837 #[tokio::test]
838 async fn runtime_module_registers_derived_entity_and_behavior() {
839 let context = UserContext::new().with_module(
840 RuntimeModule::new().entity_with_behavior::<CatalogProductRow, _>(OrderBehavior),
841 );
842 assert!(context.entity("CatalogProduct").is_some());
843 assert!(context.has_entity_data_service("CatalogProduct"));
844 assert!(
845 context
846 .entity_data_service_behavior("CatalogProduct")
847 .is_some()
848 );
849 }
850
851 #[tokio::test]
852 async fn module_macro_registers_multiple_entities() {
853 let context = UserContext::new().with_module(crate::module!(CatalogProductRow));
854 assert!(context.entity("CatalogProduct").is_some());
855 assert!(context.has_entity_data_service("CatalogProduct"));
856 }
857
858 #[tokio::test]
859 async fn module_macro_registers_entity_behavior_pairs() {
860 let context =
861 UserContext::new().with_module(crate::module!(CatalogProductRow => OrderBehavior));
862 assert!(context.entity("CatalogProduct").is_some());
863 assert!(
864 context
865 .entity_data_service_behavior("CatalogProduct")
866 .is_some()
867 );
868 }
869
870 #[tokio::test]
871 async fn data_service_returns_optimistic_lock_conflict() {
872 let store = InMemoryMetadataStore::new().with_entity(entity());
873 let executor = StubExecutor {
874 affected: 0,
875 rows: Vec::new(),
876 };
877 let repo = RuntimeDataService::new(&store, &executor);
878
879 let err = repo
880 .update(
881 &UpdateCommand::new("Order", 1_u64)
882 .expected_version(3)
883 .value("name", "next"),
884 )
885 .await
886 .unwrap_err();
887
888 match err {
889 DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. }) => {}
890 other => panic!("unexpected error: {other}"),
891 }
892 }
893
894 #[tokio::test]
895 async fn user_context_indexes_resources_and_locals() {
896 let mut context =
897 UserContext::new().with_metadata(InMemoryMetadataStore::new().with_entity(entity()));
898 context.insert_resource::<u64>(42);
899 context.insert_named_resource("tenant", String::from("acme"));
900 context.put_local("trace_id", "req-1");
901
902 assert!(context.entity("Order").is_some());
903 assert_eq!(context.get_resource::<u64>(), Some(&42));
904 assert_eq!(
905 context.get_named_resource::<String>("tenant"),
906 Some(&String::from("acme"))
907 );
908 assert_eq!(
909 context.local("trace_id"),
910 Some(&Value::Text("req-1".to_owned()))
911 );
912 }
913
914 #[tokio::test]
915 async fn user_context_builds_context_data_service() {
916 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
917 let mut context = UserContext::new()
918 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
919 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
920 telemetry_events.clone(),
921 )));
922 context.insert_resource(PostgresDialect);
923 context.insert_resource(StubExecutor {
924 affected: 1,
925 rows: Vec::new(),
926 });
927
928 let repo = context.data_service_internal::<StubExecutor>().unwrap();
929 let affected = repo
930 .update(
931 &UpdateCommand::new("Order", 1_u64)
932 .expected_version(3)
933 .value("name", "next"),
934 )
935 .await
936 .unwrap();
937
938 assert_eq!(affected, 1);
939 assert_eq!(
940 telemetry_events.lock().unwrap().as_slice(),
941 ["start:mutation", "start:provider", "success", "success"]
942 );
943 }
944
945 #[tokio::test]
946 async fn user_context_resolves_entity_data_service_by_entity_type() {
947 let mut context = UserContext::new()
948 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
949 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
950 context.insert_resource(PostgresDialect);
951 context.insert_resource(StubExecutor {
952 affected: 1,
953 rows: Vec::new(),
954 });
955
956 let repo = context
957 .entity_data_service::<StubExecutor>("Order")
958 .unwrap();
959 assert_eq!(repo.entity(), "Order");
960 assert_eq!(repo.select().entity, "Order");
961
962 let affected = repo
963 .insert_internal(
964 &repo
965 .insert_command()
966 .value("id", 1_u64)
967 .value("version", 1_i64)
968 .value("name", "n"),
969 )
970 .await
971 .unwrap();
972 assert_eq!(affected, 1);
973 }
974
975 #[tokio::test]
976 async fn entity_data_service_applies_behavior_hooks() {
977 let mut context = UserContext::new()
978 .with_metadata(
979 InMemoryMetadataStore::new()
980 .with_entity(entity())
981 .with_entity(line_entity())
982 .with_entity(product_entity()),
983 )
984 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
985 .with_entity_data_service_behavior_registry(
986 InMemoryEntityDataServiceBehaviorRegistry::new()
987 .with_behavior("Order", OrderBehavior),
988 );
989 context.insert_resource(PostgresDialect);
990 context.insert_resource(StubExecutor {
991 affected: 1,
992 rows: Vec::new(),
993 });
994
995 let repo = context
996 .entity_data_service::<StubExecutor>("Order")
997 .unwrap();
998
999 let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
1003 let affected = repo.insert_internal(&insert).await.unwrap();
1004 assert_eq!(affected, 1);
1005 assert_eq!(repo.relation_loads(), vec!["lines".to_owned()]);
1006 }
1007
1008 #[tokio::test]
1009 async fn entity_data_service_applies_request_policy_after_behavior_hooks() {
1010 let mut context = UserContext::new()
1011 .with_metadata(
1012 InMemoryMetadataStore::new()
1013 .with_entity(entity())
1014 .with_entity(line_entity())
1015 .with_entity(product_entity()),
1016 )
1017 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1018 .with_entity_data_service_behavior_registry(
1019 InMemoryEntityDataServiceBehaviorRegistry::new()
1020 .with_behavior("Order", OrderBehavior),
1021 )
1022 .with_request_policy(TenantRequestPolicy);
1023 context.insert_named_resource("tenant_id", 9_u64);
1024 context.insert_resource(PostgresDialect);
1025 context.insert_resource(StubExecutor {
1026 affected: 1,
1027 rows: Vec::new(),
1028 });
1029
1030 let repo = context
1031 .entity_data_service::<StubExecutor>("Order")
1032 .unwrap();
1033
1034 let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
1039 let command = repo.prepare_insert_command(&insert).unwrap();
1040 assert_eq!(command.values.get("version"), Some(&Value::I64(9)));
1041 }
1042
1043 #[tokio::test]
1044 async fn entity_data_service_prepares_insert_command_with_generated_id() {
1045 let mut context = UserContext::new()
1046 .with_metadata(
1047 InMemoryMetadataStore::new()
1048 .with_entity(entity())
1049 .with_entity(line_entity())
1050 .with_entity(product_entity()),
1051 )
1052 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1053 .with_entity_data_service_behavior_registry(
1054 InMemoryEntityDataServiceBehaviorRegistry::new()
1055 .with_behavior("Order", OrderBehavior),
1056 )
1057 .with_internal_id_generator(FixedIdGenerator(42));
1058 context.insert_resource(PostgresDialect);
1059 context.insert_resource(StubExecutor {
1060 affected: 1,
1061 rows: Vec::new(),
1062 });
1063
1064 let repo = context
1065 .entity_data_service::<StubExecutor>("Order")
1066 .unwrap();
1067
1068 let prepared = repo
1069 .prepare_insert_command(&repo.insert_command().value("id", 0_u64).value("name", "n"))
1070 .unwrap();
1071
1072 assert_eq!(prepared.values.get("id"), Some(&Value::U64(42)));
1073 assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
1074 assert_eq!(
1075 prepared.values.get("name"),
1076 Some(&Value::Text("n".to_owned()))
1077 );
1078
1079 let prepared_zero_version = repo
1080 .prepare_insert_command(
1081 &repo
1082 .insert_command()
1083 .value("id", 0_u64)
1084 .value("version", 0_i64)
1085 .value("name", "zero-version"),
1086 )
1087 .unwrap();
1088 assert_eq!(
1089 prepared_zero_version.values.get("version"),
1090 Some(&Value::I64(1))
1091 );
1092 }
1093
1094 #[tokio::test]
1095 async fn custom_user_context_can_drive_insert_preparation() {
1096 let mut context = UserContext::new()
1097 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1098 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1099 .with_entity_data_service_behavior_registry(
1100 InMemoryEntityDataServiceBehaviorRegistry::new()
1101 .with_behavior("Order", ContextAwareOrderBehavior),
1102 )
1103 .with_internal_id_generator(FixedIdGenerator(99));
1104 context.insert_named_resource("tenant", String::from("acme"));
1105 context.insert_named_resource("initial_version", 7_i64);
1106 context.put_local("trace_id", "req-9");
1107 context.insert_resource(PostgresDialect);
1108 context.insert_resource(StubExecutor {
1109 affected: 1,
1110 rows: Vec::new(),
1111 });
1112
1113 let repo = context
1114 .entity_data_service::<StubExecutor>("Order")
1115 .unwrap();
1116 let prepared = repo.prepare_insert_command(&repo.insert_command()).unwrap();
1117
1118 assert_eq!(prepared.values.get("id"), Some(&Value::U64(99)));
1119 assert_eq!(prepared.values.get("version"), Some(&Value::I64(7)));
1120 assert_eq!(
1121 prepared.values.get("name"),
1122 Some(&Value::Text("acme:req-9".to_owned()))
1123 );
1124 }
1125
1126 #[tokio::test]
1127 async fn checker_registry_validates_and_fixes_insert_commands() {
1128 let mut context = UserContext::new()
1129 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1130 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1131 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1132 .with_internal_id_generator(FixedIdGenerator(77));
1133 context.insert_resource(PostgresDialect);
1134 context.insert_resource(StubExecutor {
1135 affected: 1,
1136 rows: Vec::new(),
1137 });
1138
1139 let repo = context
1140 .entity_data_service::<StubExecutor>("Order")
1141 .unwrap();
1142 let prepared = repo
1143 .prepare_insert_command(&repo.insert_command().value("name", "valid"))
1144 .unwrap();
1145
1146 assert_eq!(prepared.values.get("id"), Some(&Value::U64(77)));
1147 assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
1148 assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1149
1150 let error = repo
1151 .prepare_insert_command(&repo.insert_command().value("name", "no"))
1152 .unwrap_err();
1153 match error {
1154 RuntimeError::Check(results) => {
1155 assert_eq!(results.len(), 1);
1156 assert_eq!(results[0].location.to_string(), "name");
1157 }
1158 other => panic!("unexpected checker error: {other:?}"),
1159 }
1160 }
1161
1162 #[test]
1163 fn metadata_not_null_constraints_are_checked_without_a_custom_checker() {
1164 let context = UserContext::new().with_metadata(
1165 InMemoryMetadataStore::new().with_entity(
1166 EntityDescriptor::new("School")
1167 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1168 .property(PropertyDescriptor::new("contact_phone", DataType::Text).not_null()),
1169 ),
1170 );
1171 let mut record = Record::from([
1172 ("id".to_owned(), Value::U64(1)),
1173 (
1174 CHECK_OBJECT_STATUS_FIELD.to_owned(),
1175 Value::from(CheckObjectStatus::Create),
1176 ),
1177 ]);
1178
1179 let error = context
1180 .check_and_fix_record("School", &mut record)
1181 .unwrap_err();
1182
1183 match error {
1184 RuntimeError::Check(results) => {
1185 assert_eq!(results.len(), 1);
1186 assert_eq!(results[0].rule, CheckRule::Required);
1187 assert_eq!(results[0].location.to_string(), "contact_phone");
1188 }
1189 other => panic!("unexpected validation error: {other:?}"),
1190 }
1191 }
1192
1193 #[test]
1194 fn metadata_not_null_constraints_allow_omitted_fields_on_partial_update() {
1195 let context = UserContext::new().with_metadata(
1196 InMemoryMetadataStore::new().with_entity(
1197 EntityDescriptor::new("School")
1198 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1199 .property(PropertyDescriptor::new("contact_phone", DataType::Text).not_null()),
1200 ),
1201 );
1202 let mut record = Record::from([
1203 ("id".to_owned(), Value::U64(1)),
1204 (
1205 CHECK_OBJECT_STATUS_FIELD.to_owned(),
1206 Value::from(CheckObjectStatus::Update),
1207 ),
1208 ]);
1209
1210 context.check_and_fix_record("School", &mut record).unwrap();
1211
1212 record.insert("contact_phone".to_owned(), Value::Null);
1213 assert!(matches!(
1214 context.check_and_fix_record("School", &mut record),
1215 Err(RuntimeError::Check(_))
1216 ));
1217 }
1218
1219 #[tokio::test]
1220 async fn typed_checker_validates_and_fixes_derived_entities_without_record_access() {
1221 let mut context = UserContext::new()
1222 .with_metadata(InMemoryMetadataStore::new().with_entity(Order::entity_descriptor()))
1223 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1224 .with_checker_registry(
1225 InMemoryCheckerRegistry::new()
1226 .with_checker(TypedEntityChecker::<Order, _>::new(TypedOrderChecker)),
1227 )
1228 .with_internal_id_generator(FixedIdGenerator(79));
1229 context.insert_resource(PostgresDialect);
1230 context.insert_resource(StubExecutor {
1231 affected: 1,
1232 rows: Vec::new(),
1233 });
1234
1235 let repo = context
1236 .entity_data_service::<StubExecutor>("Order")
1237 .unwrap();
1238 let prepared = repo
1239 .prepare_insert_command(
1240 &repo
1241 .insert_command()
1242 .value("name", "fix")
1243 .value("version", 1_i64),
1244 )
1245 .unwrap();
1246 assert_eq!(
1247 prepared.values.get("name"),
1248 Some(&Value::Text("fixed".to_owned()))
1249 );
1250 assert_eq!(prepared.values.get("id"), Some(&Value::U64(79)));
1251 assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1252
1253 let error = repo
1254 .prepare_insert_command(&repo.insert_command().value("version", 1_i64))
1255 .unwrap_err();
1256 match error {
1257 RuntimeError::Check(results) => {
1258 assert!(
1259 results
1260 .iter()
1261 .any(|result| result.rule == CheckRule::Required
1262 && result.location.to_string() == "name")
1263 );
1264 }
1265 other => panic!("unexpected typed checker error: {other:?}"),
1266 }
1267 }
1268
1269 #[tokio::test]
1270 async fn checker_registry_reports_nested_create_locations_and_fixes_records() {
1271 let context = UserContext::new()
1272 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker));
1273
1274 let mut child = Record::from([
1275 (String::from("id"), Value::U64(10)),
1276 (
1277 String::from(CHECK_OBJECT_STATUS_FIELD),
1278 Value::from(CheckObjectStatus::Create),
1279 ),
1280 ]);
1281 let error = context
1282 .check_and_fix_record_at(
1283 "Order",
1284 &mut child,
1285 &ObjectLocation::hash_root("lines").element(0),
1286 )
1287 .unwrap_err();
1288
1289 assert_eq!(child.get("version"), Some(&Value::I64(1)));
1290 match error {
1291 RuntimeError::Check(results) => {
1292 assert_eq!(results.len(), 1);
1293 assert_eq!(results[0].rule, CheckRule::Required);
1294 assert_eq!(results[0].location.to_string(), "lines[0].name");
1295 }
1296 other => panic!("unexpected checker error: {other:?}"),
1297 }
1298
1299 child.insert("name".to_owned(), Value::Text("valid child".to_owned()));
1300 context
1301 .check_and_fix_record_at(
1302 "Order",
1303 &mut child,
1304 &ObjectLocation::hash_root("lines").element(0),
1305 )
1306 .unwrap();
1307 }
1308
1309 #[tokio::test]
1310 async fn built_in_language_translators_cover_fifteen_languages() {
1311 assert_eq!(Language::ALL.len(), 15);
1312 let results = [
1313 super::CheckResult::required(ObjectLocation::hash_root("name")),
1314 super::CheckResult::min(ObjectLocation::hash_root("age"), 18_i64, 12_i64),
1315 super::CheckResult::max(ObjectLocation::hash_root("age"), 65_i64, 70_i64),
1316 super::CheckResult::min_str(ObjectLocation::hash_root("name"), 2, "x"),
1317 super::CheckResult::max_str(ObjectLocation::hash_root("name"), 8, "too long name"),
1318 ];
1319 let messages = Language::ALL
1320 .iter()
1321 .flat_map(|language| {
1322 results
1323 .iter()
1324 .map(|result| translate_check_result(*language, result))
1325 })
1326 .collect::<Vec<_>>();
1327
1328 assert_eq!(messages.len(), 75);
1329 assert!(messages.iter().all(|message| !message.is_empty()));
1330 assert!(messages.iter().all(|message| !message.contains('{')));
1331 assert!(messages.iter().any(|message| message.contains("required")));
1332 assert!(messages.iter().any(|message| message.contains("å¿…å¡«")));
1333 assert!(
1334 messages
1335 .iter()
1336 .any(|message| message.contains("obligatoire"))
1337 );
1338 assert_eq!(Language::from_code("zh-CN"), Some(Language::Chinese));
1339 assert_eq!(
1340 Language::from_code("zh-TW"),
1341 Some(Language::TraditionalChinese)
1342 );
1343 }
1344
1345 #[tokio::test]
1346 async fn user_context_language_switch_translates_checker_errors() {
1347 let mut context = UserContext::new()
1348 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1349 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1350 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1351 .with_internal_id_generator(FixedIdGenerator(77))
1352 .with_language(Language::Chinese);
1353 context.insert_resource(PostgresDialect);
1354 context.insert_resource(StubExecutor {
1355 affected: 1,
1356 rows: Vec::new(),
1357 });
1358
1359 let repo = context
1360 .entity_data_service::<StubExecutor>("Order")
1361 .unwrap();
1362 let error = repo
1363 .prepare_insert_command(&repo.insert_command())
1364 .unwrap_err();
1365 match error {
1366 RuntimeError::Check(results) => {
1367 assert_eq!(results.len(), 1);
1368 assert!(
1369 results[0]
1370 .message
1371 .as_ref()
1372 .is_some_and(|message| message.contains("å¿…å¡«"))
1373 );
1374 }
1375 other => panic!("unexpected checker error: {other:?}"),
1376 }
1377
1378 let mut context = UserContext::new().with_language(Language::English);
1379 context.set_language_code("es").unwrap();
1380 assert_eq!(context.language(), Language::Spanish);
1381 assert!(context.set_locale_code("invalid-code").is_err());
1382 assert_eq!(context.language(), Language::Spanish);
1383
1384 let catalog = I18nCatalog::from_json(
1385 r#"{
1386 "schema":"teaql.i18n/v1",
1387 "defaultLocale":"en",
1388 "locales":{
1389 "en":{"messages":{"checker.required":"EN {location}"},"vocabulary":{}},
1390 "es":{"messages":{"checker.required":"ES {location}"},"vocabulary":{}}
1391 }
1392 }"#,
1393 )
1394 .unwrap();
1395 context.set_i18n_catalog(Arc::new(catalog));
1396 let mut results = vec![super::CheckResult::required(ObjectLocation::hash_root(
1397 "name",
1398 ))];
1399 context.translate_check_results(&mut results);
1400 assert_eq!(results[0].message.as_deref(), Some("ES Name"));
1401 }
1402
1403 #[tokio::test]
1404 async fn user_context_event_sink_receives_data_service_mutation_events() {
1405 let events = Arc::new(Mutex::new(Vec::new()));
1406 let safe_events = Arc::new(Mutex::new(Vec::new()));
1407 let mut context = UserContext::new()
1408 .with_metadata(
1409 InMemoryMetadataStore::new()
1410 .with_entity(entity().audit_mask_fields(vec!["name".to_owned()])),
1411 )
1412 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1413 .with_internal_id_generator(FixedIdGenerator(88))
1414 .with_event_sink(RecordingEventSink {
1415 events: events.clone(),
1416 })
1417 .with_custom_event_sink(RecordingSafeEventSink {
1418 events: safe_events.clone(),
1419 });
1420 context.insert_resource(PostgresDialect);
1421 context.insert_resource(StubExecutor {
1422 affected: 1,
1423 rows: vec![Record::from([
1424 ("id".to_owned(), Value::U64(88)),
1425 ("version".to_owned(), Value::I64(1)),
1426 ("name".to_owned(), Value::Text("old".to_owned())),
1427 ])],
1428 });
1429
1430 let repo = context
1431 .entity_data_service::<StubExecutor>("Order")
1432 .unwrap();
1433 repo.insert_internal(&repo.insert_command().value("name", "created"))
1434 .await
1435 .unwrap();
1436 repo.update_internal(
1437 &repo
1438 .update_command(88_u64)
1439 .expected_version(1)
1440 .value("name", "updated"),
1441 )
1442 .await
1443 .unwrap();
1444 repo.delete_internal(&repo.delete_command(88_u64).expected_version(2))
1445 .await
1446 .unwrap();
1447 repo.recover_internal(&repo.recover_command(88_u64, -3))
1448 .await
1449 .unwrap();
1450
1451 let events = events.lock().unwrap();
1452 assert_eq!(events.len(), 4);
1453 assert_eq!(events[0].kind, RawAuditEventKind::Created);
1454 assert_eq!(events[0].entity, "Order");
1455 assert_eq!(events[0].values.get("id"), Some(&Value::U64(88)));
1456 assert_eq!(events[1].kind, RawAuditEventKind::Updated);
1457 assert_eq!(events[1].values.get("id"), Some(&Value::U64(88)));
1458 assert_eq!(events[1].values.get("version"), Some(&Value::I64(2)));
1459 assert_eq!(events[1].updated_fields, vec!["name".to_owned()]);
1460 assert_eq!(
1461 events[1]
1462 .old_values
1463 .as_ref()
1464 .and_then(|values| values.get("name")),
1465 None );
1467 assert_eq!(
1468 events[1]
1469 .new_values
1470 .as_ref()
1471 .and_then(|values| values.get("name")),
1472 Some(&Value::Text("updated".to_owned()))
1473 );
1474 assert_eq!(events[1].changes.len(), 1);
1475 assert_eq!(events[1].changes[0].field, "name");
1476 assert_eq!(
1477 events[1].changes[0].old_value,
1478 None );
1480 assert_eq!(
1481 events[1].changes[0].new_value,
1482 Some(Value::Text("updated".to_owned()))
1483 );
1484 assert_eq!(events[2].kind, RawAuditEventKind::Deleted);
1485 assert!(events[2].old_values.is_none()); assert!(events[2].new_values.is_none());
1487 assert_eq!(events[3].kind, RawAuditEventKind::Recovered);
1488 assert_eq!(
1489 events[3]
1490 .old_values
1491 .as_ref()
1492 .and_then(|values| values.get("version")),
1493 None );
1495 assert_eq!(
1496 events[3]
1497 .new_values
1498 .as_ref()
1499 .and_then(|values| values.get("version")),
1500 Some(&Value::I64(4))
1501 );
1502 assert_eq!(events[3].changes[0].field, "version");
1503 drop(events);
1504
1505 let safe_events = safe_events.lock().unwrap();
1506 assert_eq!(safe_events.len(), 4);
1507 assert_eq!(safe_events[0].kind, RawAuditEventKind::Created);
1508 let name = safe_events[0]
1509 .fields
1510 .iter()
1511 .find(|field| field.name == "name")
1512 .expect("application audit event should contain the changed name field");
1513 assert!(name.masked);
1514 assert_ne!(name.value.as_deref(), Some("created"));
1515 }
1516
1517 #[tokio::test]
1518 async fn entity_data_service_builds_relation_plans() {
1519 let mut context = UserContext::new()
1520 .with_metadata(
1521 InMemoryMetadataStore::new()
1522 .with_entity(entity())
1523 .with_entity(line_entity())
1524 .with_entity(product_entity()),
1525 )
1526 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1527 .with_entity_data_service_behavior_registry(
1528 InMemoryEntityDataServiceBehaviorRegistry::new()
1529 .with_behavior("Order", OrderBehavior),
1530 );
1531 context.insert_resource(PostgresDialect);
1532 context.insert_resource(StubExecutor {
1533 affected: 1,
1534 rows: Vec::new(),
1535 });
1536
1537 let repo = context
1538 .entity_data_service::<StubExecutor>("Order")
1539 .unwrap();
1540 let plans = repo.relation_plans().unwrap();
1541
1542 assert_eq!(plans.len(), 1);
1543 assert_eq!(plans[0].relation_name, "lines");
1544 assert_eq!(plans[0].target_entity, "OrderLine");
1545 assert_eq!(plans[0].local_key, "id");
1546 assert_eq!(plans[0].foreign_key, "order_id");
1547 assert!(plans[0].many);
1548 }
1549
1550 #[tokio::test]
1551 async fn entity_data_service_builds_relation_query_from_parent_rows() {
1552 let mut context = UserContext::new()
1553 .with_metadata(
1554 InMemoryMetadataStore::new()
1555 .with_entity(entity())
1556 .with_entity(line_entity())
1557 .with_entity(product_entity()),
1558 )
1559 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1560 .with_entity_data_service_behavior_registry(
1561 InMemoryEntityDataServiceBehaviorRegistry::new()
1562 .with_behavior("Order", OrderBehavior),
1563 );
1564 context.insert_resource(PostgresDialect);
1565 context.insert_resource(StubExecutor {
1566 affected: 1,
1567 rows: Vec::new(),
1568 });
1569
1570 let repo = context
1571 .entity_data_service::<StubExecutor>("Order")
1572 .unwrap();
1573 let parent_rows = vec![
1574 Record::from([(String::from("id"), Value::U64(11))]),
1575 Record::from([(String::from("id"), Value::U64(12))]),
1576 ];
1577
1578 let query = repo.relation_query("lines", &parent_rows).unwrap();
1579 }
1584
1585 #[tokio::test]
1586 async fn entity_data_service_enhances_parent_rows_with_relations() {
1587 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
1588 let mut context = UserContext::new()
1589 .with_metadata(
1590 InMemoryMetadataStore::new()
1591 .with_entity(entity())
1592 .with_entity(line_entity())
1593 .with_entity(product_entity()),
1594 )
1595 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1596 .with_entity_data_service_behavior_registry(
1597 InMemoryEntityDataServiceBehaviorRegistry::new()
1598 .with_behavior("Order", OrderBehavior),
1599 )
1600 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
1601 telemetry_events.clone(),
1602 )));
1603 context.insert_resource(PostgresDialect);
1604 context.insert_resource(StubExecutor {
1605 affected: 1,
1606 rows: vec![
1607 Record::from([
1608 (String::from("id"), Value::U64(101)),
1609 (String::from("order_id"), Value::U64(11)),
1610 (String::from("name"), Value::Text(String::from("l1"))),
1611 ]),
1612 Record::from([
1613 (String::from("id"), Value::U64(102)),
1614 (String::from("order_id"), Value::U64(11)),
1615 (String::from("name"), Value::Text(String::from("l2"))),
1616 ]),
1617 Record::from([
1618 (String::from("id"), Value::U64(201)),
1619 (String::from("order_id"), Value::U64(12)),
1620 (String::from("name"), Value::Text(String::from("l3"))),
1621 ]),
1622 ],
1623 });
1624
1625 let repo = context
1626 .entity_data_service::<StubExecutor>("Order")
1627 .unwrap();
1628 let mut parents = vec![
1629 Record::from([(String::from("id"), Value::U64(11))]),
1630 Record::from([(String::from("id"), Value::U64(12))]),
1631 ];
1632
1633 repo.enhance_relations_internal(&mut parents).await.unwrap();
1634
1635 match parents[0].get("lines") {
1636 Some(Value::List(lines)) => assert_eq!(lines.len(), 2),
1637 other => panic!("unexpected lines payload: {other:?}"),
1638 }
1639 match parents[1].get("lines") {
1640 Some(Value::List(lines)) => assert_eq!(lines.len(), 1),
1641 other => panic!("unexpected lines payload: {other:?}"),
1642 }
1643 assert!(
1644 telemetry_events
1645 .lock()
1646 .unwrap()
1647 .iter()
1648 .any(|event| event == "start:relation_load")
1649 );
1650 }
1651
1652 #[tokio::test]
1653 async fn relation_limit_is_partitioned_per_parent_and_rank_is_internal() {
1654 let mut rows = Vec::new();
1655 for (order_id, first_line_id) in [(11_u64, 101_u64), (12_u64, 201_u64)] {
1656 for rank in 1_u64..=3 {
1657 rows.push(Record::from([
1658 (String::from("id"), Value::U64(first_line_id + rank - 1)),
1659 (String::from("order_id"), Value::U64(order_id)),
1660 (
1661 String::from(teaql_core::PARTITION_RANK_PROPERTY),
1662 Value::U64(rank),
1663 ),
1664 ]));
1665 }
1666 }
1667
1668 let mut context = UserContext::new()
1669 .with_metadata(
1670 InMemoryMetadataStore::new()
1671 .with_entity(entity())
1672 .with_entity(line_entity()),
1673 )
1674 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1675 context.insert_resource(PostgresDialect);
1676 context.insert_resource(CapturingQueryExecutor {
1677 rows,
1678 queries: Mutex::new(Vec::new()),
1679 });
1680
1681 let repo = context
1682 .entity_data_service::<CapturingQueryExecutor>("Order")
1683 .unwrap();
1684 let mut parents = vec![
1685 Record::from([(String::from("id"), Value::U64(11))]),
1686 Record::from([(String::from("id"), Value::U64(12))]),
1687 ];
1688 let query = SelectQuery::new("Order").relation_query(
1689 "lines",
1690 SelectQuery::new("OrderLine")
1691 .order_by(OrderBy::desc("id"))
1692 .limit(3),
1693 );
1694
1695 repo.enhance_query_relations_internal(&mut parents, &query)
1696 .await
1697 .unwrap();
1698
1699 let captured = &context
1700 .get_resource::<CapturingQueryExecutor>()
1701 .unwrap()
1702 .queries
1703 .lock()
1704 .unwrap()[0];
1705 assert_eq!(captured.partition_by.as_deref(), Some("order_id"));
1706 assert_eq!(captured.slice.and_then(|slice| slice.limit), Some(3));
1707 for parent in &parents {
1708 let Some(Value::List(lines)) = parent.get("lines") else {
1709 panic!("missing relation lines")
1710 };
1711 assert_eq!(lines.len(), 3);
1712 assert!(lines.iter().all(|line| match line {
1713 Value::Object(line) => !line.contains_key(teaql_core::PARTITION_RANK_PROPERTY),
1714 _ => false,
1715 }));
1716 }
1717 }
1718
1719 #[tokio::test]
1720 async fn relation_enhancement_wraps_inverse_many_relation_as_list() {
1721 let mut context = UserContext::new()
1722 .with_metadata(
1723 InMemoryMetadataStore::new()
1724 .with_entity(OrderLineWithProductEntityRow::entity_descriptor())
1725 .with_entity(ProductWithLinesEntityRow::entity_descriptor()),
1726 )
1727 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderLine"));
1728 context.insert_resource(PostgresDialect);
1729 context.insert_resource(QueueExecutor {
1730 affected: 1,
1731 rows: Mutex::new(VecDeque::from([
1732 vec![Record::from([
1733 (String::from("id"), Value::U64(11)),
1734 (String::from("order_id"), Value::U64(7)),
1735 (String::from("name"), Value::Text(String::from("line"))),
1736 (String::from("product_id"), Value::U64(101)),
1737 ])],
1738 vec![Record::from([
1739 (String::from("id"), Value::U64(101)),
1740 (String::from("name"), Value::Text(String::from("sku"))),
1741 ])],
1742 ])),
1743 queries: Mutex::new(Vec::new()),
1744 });
1745
1746 let repo = context
1747 .entity_data_service::<QueueExecutor>("OrderLine")
1748 .unwrap();
1749 let rows = repo
1750 .fetch_enhanced_entities_internal::<OrderLineWithProductEntityRow>(
1751 &SelectQuery::new("OrderLine").relation("product"),
1752 )
1753 .await
1754 .unwrap();
1755
1756 let product = rows.data[0].product.as_ref().unwrap();
1757 assert_eq!(product.lines.data.len(), 1);
1758 assert_eq!(product.lines.data[0].id, 11);
1759 }
1760
1761 #[tokio::test]
1762 async fn entity_data_service_fetches_smart_list_of_entities() {
1763 let mut context = UserContext::new()
1764 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1765 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1766 context.insert_resource(PostgresDialect);
1767 context.insert_resource(StubExecutor {
1768 affected: 1,
1769 rows: vec![Record::from([
1770 (String::from("id"), Value::U64(7)),
1771 (String::from("version"), Value::I64(2)),
1772 (String::from("name"), Value::Text(String::from("typed"))),
1773 ])],
1774 });
1775
1776 let repo = context
1777 .entity_data_service::<StubExecutor>("Order")
1778 .unwrap();
1779 let rows = repo
1780 .fetch_entities_internal::<OrderEntity>(&repo.select())
1781 .await
1782 .unwrap();
1783
1784 assert_eq!(rows.len(), 1);
1785 assert_eq!(
1786 rows.first(),
1787 Some(&OrderEntity {
1788 id: 7,
1789 version: 2,
1790 name: String::from("typed"),
1791 })
1792 );
1793 }
1794
1795 #[tokio::test]
1796 async fn entity_data_service_fetches_smart_list_of_derived_entities() {
1797 let mut context = UserContext::new()
1798 .with_metadata(
1799 InMemoryMetadataStore::new().with_entity(CatalogProductRow::entity_descriptor()),
1800 )
1801 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("CatalogProduct"));
1802 context.insert_resource(PostgresDialect);
1803 context.insert_resource(StubExecutor {
1804 affected: 1,
1805 rows: vec![Record::from([
1806 (String::from("id"), Value::U64(9)),
1807 (String::from("name"), Value::Text(String::from("derived"))),
1808 ])],
1809 });
1810
1811 let repo = context
1812 .entity_data_service::<StubExecutor>("CatalogProduct")
1813 .unwrap();
1814 let rows = repo
1815 .fetch_entities_internal::<CatalogProductRow>(&repo.select())
1816 .await
1817 .unwrap();
1818
1819 assert_eq!(rows.len(), 1);
1820 assert_eq!(
1821 rows.first(),
1822 Some(&CatalogProductRow {
1823 id: 9,
1824 name: String::from("derived"),
1825 })
1826 );
1827 }
1828
1829 #[tokio::test]
1830 async fn entity_data_service_collects_dynamic_properties_for_aggregate_output() {
1831 let mut context = UserContext::new()
1832 .with_metadata(
1833 InMemoryMetadataStore::new()
1834 .with_entity(OrderAggregateDynamic::entity_descriptor()),
1835 )
1836 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderAggregate"));
1837 context.insert_resource(PostgresDialect);
1838 context.insert_resource(StubExecutor {
1839 affected: 1,
1840 rows: vec![Record::from([
1841 (String::from("id"), Value::U64(1)),
1842 (String::from("lineCount"), Value::I64(3)),
1843 (String::from("amount"), Value::F64(18.5)),
1844 ])],
1845 });
1846
1847 let repo = context
1848 .entity_data_service::<StubExecutor>("OrderAggregate")
1849 .unwrap();
1850 let rows = repo
1851 .fetch_entities_internal::<OrderAggregateDynamic>(&repo.select())
1852 .await
1853 .unwrap();
1854
1855 assert_eq!(rows.len(), 1);
1856 assert_eq!(rows.data[0].id, 1);
1857 assert_eq!(rows.data[0].dynamic.get("lineCount"), Some(&Value::I64(3)));
1858 assert_eq!(rows.data[0].dynamic.get("amount"), Some(&Value::F64(18.5)));
1859 assert_eq!(
1860 rows.into_vec().into_iter().next().unwrap().into_json(),
1861 serde_json::json!({
1862 "id": 1,
1863 "lineCount": 3,
1864 "amount": 18.5
1865 })
1866 );
1867 }
1868
1869 #[tokio::test]
1870 async fn entity_data_service_executes_relation_aggregates_into_dynamic_properties() {
1871 let executor = QueueExecutor {
1872 affected: 1,
1873 rows: Mutex::new(VecDeque::from([
1874 vec![
1875 Record::from([
1876 (String::from("id"), Value::U64(1)),
1877 (String::from("version"), Value::I64(1)),
1878 (String::from("name"), Value::Text(String::from("first"))),
1879 ]),
1880 Record::from([
1881 (String::from("id"), Value::U64(2)),
1882 (String::from("version"), Value::I64(1)),
1883 (String::from("name"), Value::Text(String::from("second"))),
1884 ]),
1885 ],
1886 vec![Record::from([
1887 (String::from("order_id"), Value::U64(1)),
1888 (String::from("lineCount"), Value::I64(3)),
1889 ])],
1890 ])),
1891 queries: Mutex::new(Vec::new()),
1892 };
1893 let mut context = UserContext::new()
1894 .with_metadata(
1895 InMemoryMetadataStore::new()
1896 .with_entity(entity())
1897 .with_entity(line_entity()),
1898 )
1899 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1900 context.insert_resource(PostgresDialect);
1901 context.insert_resource(executor);
1902
1903 let repo = context
1904 .entity_data_service::<QueueExecutor>("Order")
1905 .unwrap();
1906 let rows = repo
1907 .fetch_all_with_relation_aggregates_internal(
1908 &repo
1909 .select()
1910 .project("id")
1911 .project("version")
1912 .project("name"),
1913 &[RelationAggregate::new(
1914 "lines",
1915 "lineCount",
1916 SelectQuery::new("OrderLine"),
1917 true,
1918 )],
1919 )
1920 .await
1921 .unwrap();
1922
1923 assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
1924 assert_eq!(rows[1].get("lineCount"), Some(&Value::U64(0)));
1925
1926 let executor = context.get_resource::<QueueExecutor>().unwrap();
1927 let queries = executor.queries.lock().unwrap();
1928 assert_eq!(queries.len(), 2);
1929 assert_eq!(queries[1], "SELECT ... FROM OrderLine ...");
1930 }
1931
1932 #[tokio::test]
1933 async fn entity_data_service_maps_relation_aggregate_storage_key_to_property_key() {
1934 let mut line = line_entity();
1935 line.properties
1936 .iter_mut()
1937 .find(|property| property.name == "order_id")
1938 .unwrap()
1939 .column_name = "order_ref".to_owned();
1940 let executor = QueueExecutor {
1941 affected: 1,
1942 rows: Mutex::new(VecDeque::from([
1943 vec![Record::from([
1944 (String::from("id"), Value::U64(1)),
1945 (String::from("version"), Value::I64(1)),
1946 (String::from("name"), Value::Text(String::from("first"))),
1947 ])],
1948 vec![Record::from([
1949 (String::from("order_ref"), Value::I64(1)),
1950 (String::from("lineCount"), Value::I64(3)),
1951 ])],
1952 ])),
1953 queries: Mutex::new(Vec::new()),
1954 };
1955 let mut context = UserContext::new()
1956 .with_metadata(
1957 InMemoryMetadataStore::new()
1958 .with_entity(entity())
1959 .with_entity(line),
1960 )
1961 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1962 context.insert_resource(PostgresDialect);
1963 context.insert_resource(executor);
1964
1965 let repo = context
1966 .entity_data_service::<QueueExecutor>("Order")
1967 .unwrap();
1968 let rows = repo
1969 .fetch_all_with_relation_aggregates_internal(
1970 &repo
1971 .select()
1972 .project("id")
1973 .project("version")
1974 .project("name"),
1975 &[RelationAggregate::new(
1976 "lines",
1977 "lineCount",
1978 SelectQuery::new("OrderLine"),
1979 true,
1980 )],
1981 )
1982 .await
1983 .unwrap();
1984
1985 assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
1986 let executor = context.get_resource::<QueueExecutor>().unwrap();
1987 assert_eq!(
1988 executor.queries.lock().unwrap()[1],
1989 "SELECT ... FROM OrderLine ..."
1990 );
1991 }
1992
1993 #[tokio::test]
1994 async fn entity_data_service_uses_aggregation_cache_when_resource_is_registered() {
1995 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
1996 let executor = QueueExecutor {
1997 affected: 1,
1998 rows: Mutex::new(VecDeque::from([vec![Record::from([(
1999 String::from("count"),
2000 Value::I64(2),
2001 )])]])),
2002 queries: Mutex::new(Vec::new()),
2003 };
2004 let mut context = UserContext::new()
2005 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2006 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
2007 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
2008 telemetry_events.clone(),
2009 )));
2010 context.insert_resource(PostgresDialect);
2011 context.insert_resource(executor);
2012 context.insert_resource(InMemoryAggregationCache::default());
2013
2014 let repo = context
2015 .entity_data_service::<QueueExecutor>("Order")
2016 .unwrap();
2017 let query = repo
2018 .select()
2019 .count("count")
2020 .enable_aggregation_cache_for(60_000);
2021
2022 let first = repo.fetch_all_internal(&query).await.unwrap();
2023 let second = repo.fetch_all_internal(&query).await.unwrap();
2024
2025 assert_eq!(first, second);
2026 let executor = context.get_resource::<QueueExecutor>().unwrap();
2027 assert_eq!(executor.queries.lock().unwrap().len(), 1);
2028 let events = telemetry_events.lock().unwrap();
2029 assert_eq!(
2030 events
2031 .iter()
2032 .filter(|event| event.as_str() == "start:cache")
2033 .count(),
2034 2
2035 );
2036 assert_eq!(
2037 events
2038 .iter()
2039 .filter(|event| event.as_str() == "start:provider")
2040 .count(),
2041 1
2042 );
2043 }
2044
2045 #[tokio::test]
2046 async fn continuous_page_fetch_uses_id_seek_for_the_next_page() {
2047 let rows = (91_u64..=100)
2048 .rev()
2049 .map(|id| {
2050 Record::from([
2051 (String::from("id"), Value::U64(id)),
2052 (String::from("version"), Value::I64(1)),
2053 (String::from("name"), Value::Text(format!("order-{id}"))),
2054 ])
2055 })
2056 .collect();
2057 let mut context = UserContext::new()
2058 .with_user_identifier("tenant-1:user-1")
2059 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2060 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2061 context.insert_resource(PostgresDialect);
2062 context.insert_resource(CapturingQueryExecutor {
2063 rows,
2064 queries: Mutex::new(Vec::new()),
2065 });
2066 let repo = context
2067 .entity_data_service::<CapturingQueryExecutor>("Order")
2068 .unwrap();
2069
2070 let first = SelectQuery::new("Order")
2071 .order_desc("id")
2072 .page(0, 10)
2073 .optimize_for_continuous_page_fetch_with("recent-orders", 60);
2074 repo.fetch_all_internal(&first).await.unwrap();
2075 assert_eq!(
2076 context.continuous_page_plan().as_deref(),
2077 Some("OFFSET_FALLBACK:FIRST_PAGE")
2078 );
2079
2080 let second = SelectQuery::new("Order")
2081 .order_desc("id")
2082 .page(10, 10)
2083 .optimize_for_continuous_page_fetch_with("recent-orders", 60);
2084 repo.fetch_all_internal(&second).await.unwrap();
2085 assert_eq!(
2086 context.continuous_page_plan().as_deref(),
2087 Some("CURSOR_SEEK")
2088 );
2089 assert!(context.continuous_page_cursor_id().is_some());
2090
2091 let captured = context
2092 .get_resource::<CapturingQueryExecutor>()
2093 .unwrap()
2094 .queries
2095 .lock()
2096 .unwrap();
2097 assert_eq!(
2098 captured[1].slice.as_ref().map(|slice| slice.offset),
2099 Some(0)
2100 );
2101 assert!(format!("{:?}", captured[1].filter).contains("Lt"));
2102 assert!(format!("{:?}", captured[1].filter).contains("U64(91)"));
2103 }
2104
2105 #[tokio::test]
2106 async fn aggregation_cache_is_namespaced_and_invalidated_after_write() {
2107 let executor = QueueExecutor {
2108 affected: 1,
2109 rows: Mutex::new(VecDeque::from([
2110 vec![Record::from([(String::from("count"), Value::I64(2))])],
2111 vec![Record::from([(String::from("count"), Value::I64(3))])],
2112 ])),
2113 queries: Mutex::new(Vec::new()),
2114 };
2115 let mut context = UserContext::new()
2116 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2117 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2118 context.insert_resource(PostgresDialect);
2119 context.insert_resource(executor);
2120 context.insert_resource(
2121 Arc::new(InMemoryAggregationCache::with_namespace("tenant-a"))
2122 as Arc<dyn AggregationCacheBackend>,
2123 );
2124
2125 let repo = context
2126 .entity_data_service::<QueueExecutor>("Order")
2127 .unwrap();
2128 let query = repo
2129 .select()
2130 .count("count")
2131 .enable_aggregation_cache_for(60_000);
2132
2133 let first = repo.fetch_all_internal(&query).await.unwrap();
2134 let cached = repo.fetch_all_internal(&query).await.unwrap();
2135 repo.insert_internal(
2136 &InsertCommand::new("Order")
2137 .value("id", 9_u64)
2138 .value("version", 1_i64)
2139 .value("name", "new"),
2140 )
2141 .await
2142 .unwrap();
2143 let refreshed = repo.fetch_all_internal(&query).await.unwrap();
2144
2145 assert_eq!(first, cached);
2146 assert_ne!(cached, refreshed);
2147 let executor = context.get_resource::<QueueExecutor>().unwrap();
2148 assert_eq!(executor.queries.lock().unwrap().len(), 2);
2149 }
2150
2151 #[tokio::test]
2152 async fn aggregation_cache_propagates_to_relation_aggregates() {
2153 let parent_rows = vec![
2154 Record::from([
2155 (String::from("id"), Value::U64(1)),
2156 (String::from("version"), Value::I64(1)),
2157 (String::from("name"), Value::Text(String::from("first"))),
2158 ]),
2159 Record::from([
2160 (String::from("id"), Value::U64(2)),
2161 (String::from("version"), Value::I64(1)),
2162 (String::from("name"), Value::Text(String::from("second"))),
2163 ]),
2164 ];
2165 let aggregate_rows = vec![Record::from([
2166 (String::from("order_id"), Value::U64(1)),
2167 (String::from("lineCount"), Value::I64(3)),
2168 ])];
2169 let executor = QueueExecutor {
2170 affected: 1,
2171 rows: Mutex::new(VecDeque::from([parent_rows, aggregate_rows])),
2172 queries: Mutex::new(Vec::new()),
2173 };
2174 let mut context = UserContext::new()
2175 .with_metadata(
2176 InMemoryMetadataStore::new()
2177 .with_entity(entity())
2178 .with_entity(line_entity()),
2179 )
2180 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2181 context.insert_resource(PostgresDialect);
2182 context.insert_resource(executor);
2183 context.insert_resource(InMemoryAggregationCache::default());
2184
2185 let repo = context
2186 .entity_data_service::<QueueExecutor>("Order")
2187 .unwrap();
2188 let query = repo
2189 .select()
2190 .project("id")
2191 .project("version")
2192 .project("name")
2193 .enable_aggregation_cache_for(60_000)
2194 .propagate_aggregation_cache(60_000);
2195 let aggregate =
2196 RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
2197
2198 let first = repo
2199 .fetch_all_with_relation_aggregates_internal(&query, &[aggregate.clone()])
2200 .await
2201 .unwrap();
2202 let second = repo
2203 .fetch_all_with_relation_aggregates_internal(&query, &[aggregate])
2204 .await
2205 .unwrap();
2206
2207 assert_eq!(first, second);
2208 let executor = context.get_resource::<QueueExecutor>().unwrap();
2209 assert_eq!(executor.queries.lock().unwrap().len(), 2);
2210 }
2211
2212 #[tokio::test]
2213 async fn memory_data_service_fetches_smart_list_entities_with_query_features() {
2214 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2215 let data_service = MemoryDataService::new(metadata).with_rows(
2216 "Order",
2217 vec![
2218 Record::from([
2219 (String::from("id"), Value::U64(1)),
2220 (String::from("version"), Value::I64(1)),
2221 (String::from("name"), Value::Text(String::from("alpha"))),
2222 ]),
2223 Record::from([
2224 (String::from("id"), Value::U64(2)),
2225 (String::from("version"), Value::I64(1)),
2226 (String::from("name"), Value::Text(String::from("beta"))),
2227 ]),
2228 Record::from([
2229 (String::from("id"), Value::U64(3)),
2230 (String::from("version"), Value::I64(1)),
2231 (String::from("name"), Value::Text(String::from("gamma"))),
2232 ]),
2233 ],
2234 );
2235
2236 let query = teaql_core::SelectQuery::new("Order")
2237 .filter(Expr::Binary {
2238 left: Box::new(Expr::column("id")),
2239 op: teaql_core::BinaryOp::Gte,
2240 right: Box::new(Expr::value(2_u64)),
2241 })
2242 .order_by(OrderBy::desc("id"))
2243 .limit(1);
2244
2245 let orders = data_service.fetch_entities::<Order>(&query).unwrap();
2246
2247 assert_eq!(orders.ids(), vec![Value::U64(3)]);
2248 assert_eq!(orders.versions(), vec![1]);
2249 assert_eq!(orders.first().unwrap().name, "gamma");
2250 }
2251
2252 #[tokio::test]
2253 async fn memory_data_service_runs_relation_aggregates() {
2254 let metadata = InMemoryMetadataStore::new()
2255 .with_entity(entity())
2256 .with_entity(line_entity());
2257
2258 let data_service = MemoryDataService::new(metadata)
2259 .with_rows(
2260 "Order",
2261 vec![
2262 Record::from([
2263 (String::from("id"), Value::U64(1)),
2264 (String::from("version"), Value::I64(1)),
2265 (String::from("name"), Value::Text(String::from("first"))),
2266 ]),
2267 Record::from([
2268 (String::from("id"), Value::U64(2)),
2269 (String::from("version"), Value::I64(1)),
2270 (String::from("name"), Value::Text(String::from("second"))),
2271 ]),
2272 ],
2273 )
2274 .with_rows(
2275 "OrderLine",
2276 vec![
2277 Record::from([
2278 (String::from("id"), Value::U64(10)),
2279 (String::from("version"), Value::I64(1)),
2280 (String::from("order_id"), Value::U64(1)),
2281 (String::from("name"), Value::Text(String::from("line1"))),
2282 ]),
2283 Record::from([
2284 (String::from("id"), Value::U64(11)),
2285 (String::from("version"), Value::I64(1)),
2286 (String::from("order_id"), Value::U64(1)),
2287 (String::from("name"), Value::Text(String::from("line2"))),
2288 ]),
2289 Record::from([
2290 (String::from("id"), Value::U64(12)),
2291 (String::from("version"), Value::I64(1)),
2292 (String::from("order_id"), Value::U64(2)),
2293 (String::from("name"), Value::Text(String::from("line3"))),
2294 ]),
2295 ],
2296 );
2297
2298 let query = SelectQuery::new("Order").project("id").project("name");
2299 let aggregate =
2300 RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
2301
2302 let rows = data_service
2303 .fetch_all_with_relation_aggregates(&query, &[aggregate])
2304 .unwrap();
2305
2306 assert_eq!(rows.len(), 2);
2307
2308 let first_order = rows
2309 .iter()
2310 .find(|r| r.get("id") == Some(&Value::U64(1)))
2311 .unwrap();
2312 assert_eq!(first_order.get("lineCount"), Some(&Value::U64(2)));
2313
2314 let second_order = rows
2315 .iter()
2316 .find(|r| r.get("id") == Some(&Value::U64(2)))
2317 .unwrap();
2318 assert_eq!(second_order.get("lineCount"), Some(&Value::U64(1)));
2319 }
2320
2321 #[tokio::test]
2322 async fn memory_data_service_runs_aggregates() {
2323 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2324 let data_service = MemoryDataService::new(metadata).with_rows(
2325 "Order",
2326 vec![
2327 Record::from([
2328 (String::from("id"), Value::U64(1)),
2329 (String::from("version"), Value::I64(1)),
2330 (String::from("name"), Value::Text(String::from("alpha"))),
2331 ]),
2332 Record::from([
2333 (String::from("id"), Value::U64(2)),
2334 (String::from("version"), Value::I64(2)),
2335 (String::from("name"), Value::Text(String::from("beta"))),
2336 ]),
2337 ],
2338 );
2339
2340 let query = teaql_core::SelectQuery {
2341 hard_limit: 10_000,
2342 entity: String::from("Order"),
2343 projection: Vec::new(),
2344 expr_projection: Vec::new(),
2345 filter: None,
2346 having: None,
2347 order_by: Vec::new(),
2348 slice: None,
2349 partition_by: None,
2350 trace_chain: Vec::new(),
2351 aggregates: vec![
2352 Aggregate {
2353 function: AggregateFunction::Count,
2354 field: String::from("id"),
2355 alias: String::from("count"),
2356 },
2357 Aggregate {
2358 function: AggregateFunction::Sum,
2359 field: String::from("version"),
2360 alias: String::from("versionSum"),
2361 },
2362 ],
2363 group_by: Vec::new(),
2364 relations: Vec::new(),
2365 aggregation_cache: None,
2366 comment: None,
2367 raw_sql: None,
2368 raw_sql_search_criteria: Vec::new(),
2369 dynamic_properties: Vec::new(),
2370 raw_projections: Vec::new(),
2371 object_group_bys: Vec::new(),
2372 search_with_text: None,
2373 child_enhancements: Vec::new(),
2374 stream_config: None,
2375 continuous_page_fetch: None,
2376 };
2377
2378 let rows = data_service.fetch_all(&query).unwrap();
2379
2380 assert_eq!(rows.len(), 1);
2381 assert_eq!(rows[0].get("count"), Some(&Value::U64(2)));
2382 assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
2383 }
2384
2385 #[tokio::test]
2386 async fn memory_data_service_runs_grouped_aggregates_and_extended_filters() {
2387 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2388 let data_service = MemoryDataService::new(metadata).with_rows(
2389 "Order",
2390 vec![
2391 Record::from([
2392 (String::from("id"), Value::U64(1)),
2393 (String::from("version"), Value::I64(1)),
2394 (String::from("name"), Value::Text(String::from("alpha"))),
2395 ]),
2396 Record::from([
2397 (String::from("id"), Value::U64(2)),
2398 (String::from("version"), Value::I64(2)),
2399 (String::from("name"), Value::Text(String::from("alpha"))),
2400 ]),
2401 Record::from([
2402 (String::from("id"), Value::U64(3)),
2403 (String::from("version"), Value::I64(3)),
2404 (String::from("name"), Value::Text(String::from("tmp-beta"))),
2405 ]),
2406 ],
2407 );
2408
2409 let rows = data_service
2410 .fetch_all(
2411 &teaql_core::SelectQuery::new("Order")
2412 .filter(
2413 Expr::between("version", 1_i64, 3_i64)
2414 .and_expr(Expr::not_like("name", "tmp%"))
2415 .and_expr(Expr::not_in_list("name", vec![Value::from("deleted")])),
2416 )
2417 .group_by("name")
2418 .count("total")
2419 .sum("version", "versionSum"),
2420 )
2421 .unwrap();
2422
2423 assert_eq!(rows.len(), 1);
2424 assert_eq!(
2425 rows[0].get("name"),
2426 Some(&Value::Text(String::from("alpha")))
2427 );
2428 assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
2429 assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
2430 }
2431
2432 #[tokio::test]
2433 async fn memory_data_service_runs_extended_aggregates_and_having() {
2434 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2435 let data_service = MemoryDataService::new(metadata).with_rows(
2436 "Order",
2437 vec![
2438 Record::from([
2439 (String::from("id"), Value::U64(1)),
2440 (String::from("version"), Value::I64(1)),
2441 (String::from("name"), Value::Text(String::from("alpha"))),
2442 ]),
2443 Record::from([
2444 (String::from("id"), Value::U64(2)),
2445 (String::from("version"), Value::I64(3)),
2446 (String::from("name"), Value::Text(String::from("alpha"))),
2447 ]),
2448 Record::from([
2449 (String::from("id"), Value::U64(3)),
2450 (String::from("version"), Value::I64(7)),
2451 (String::from("name"), Value::Text(String::from("beta"))),
2452 ]),
2453 ],
2454 );
2455
2456 let rows = data_service
2457 .fetch_all(
2458 &teaql_core::SelectQuery::new("Order")
2459 .group_by("name")
2460 .count("total")
2461 .stddev("version", "stddevVersion")
2462 .var_pop("version", "varPopVersion")
2463 .bit_or("version", "bitOrVersion")
2464 .having(Expr::gt("total", 1_i64)),
2465 )
2466 .unwrap();
2467
2468 assert_eq!(rows.len(), 1);
2469 assert_eq!(
2470 rows[0].get("name"),
2471 Some(&Value::Text(String::from("alpha")))
2472 );
2473 assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
2474 assert_eq!(
2475 rows[0].get("stddevVersion").map(Value::to_json_value),
2476 Some(serde_json::Value::String(
2477 "1.4142135623730951454746218583".to_owned()
2478 ))
2479 );
2480 assert_eq!(
2481 rows[0].get("varPopVersion"),
2482 Some(&Value::Decimal(Decimal::ONE))
2483 );
2484 assert_eq!(rows[0].get("bitOrVersion"), Some(&Value::I64(3)));
2485 }
2486
2487 #[tokio::test]
2488 async fn memory_data_service_runs_sound_like_filter() {
2489 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2490 let data_service = MemoryDataService::new(metadata).with_rows(
2491 "Order",
2492 vec![
2493 Record::from([
2494 (String::from("id"), Value::U64(1)),
2495 (String::from("version"), Value::I64(1)),
2496 (String::from("name"), Value::Text(String::from("Robert"))),
2497 ]),
2498 Record::from([
2499 (String::from("id"), Value::U64(2)),
2500 (String::from("version"), Value::I64(1)),
2501 (String::from("name"), Value::Text(String::from("Rupert"))),
2502 ]),
2503 Record::from([
2504 (String::from("id"), Value::U64(3)),
2505 (String::from("version"), Value::I64(1)),
2506 (String::from("name"), Value::Text(String::from("Ashcraft"))),
2507 ]),
2508 ],
2509 );
2510
2511 let rows = data_service
2512 .fetch_all(
2513 &teaql_core::SelectQuery::new("Order")
2514 .filter(Expr::sound_like("name", "Robert"))
2515 .order_asc("id"),
2516 )
2517 .unwrap();
2518
2519 assert_eq!(rows.len(), 2);
2520 assert_eq!(rows[0].get("name"), Some(&Value::Text("Robert".to_owned())));
2521 assert_eq!(rows[1].get("name"), Some(&Value::Text("Rupert".to_owned())));
2522 }
2523
2524 #[tokio::test]
2525 async fn memory_data_service_runs_java_style_string_match_filters() {
2526 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2527 let data_service = MemoryDataService::new(metadata).with_rows(
2528 "Order",
2529 vec![
2530 Record::from([
2531 (String::from("id"), Value::U64(1)),
2532 (String::from("version"), Value::I64(1)),
2533 (String::from("name"), Value::Text(String::from("tea-order"))),
2534 ]),
2535 Record::from([
2536 (String::from("id"), Value::U64(2)),
2537 (String::from("version"), Value::I64(1)),
2538 (
2539 String::from("name"),
2540 Value::Text(String::from("coffee-order")),
2541 ),
2542 ]),
2543 Record::from([
2544 (String::from("id"), Value::U64(3)),
2545 (String::from("version"), Value::I64(1)),
2546 (
2547 String::from("name"),
2548 Value::Text(String::from("tea-archived")),
2549 ),
2550 ]),
2551 ],
2552 );
2553
2554 let rows = data_service
2555 .fetch_all(
2556 &teaql_core::SelectQuery::new("Order")
2557 .filter(
2558 Expr::contain("name", "tea")
2559 .and_expr(Expr::begin_with("name", "tea"))
2560 .and_expr(Expr::end_with("name", "order"))
2561 .and_expr(Expr::not_contain("name", "coffee"))
2562 .and_expr(Expr::not_begin_with("name", "archived"))
2563 .and_expr(Expr::not_end_with("name", "draft")),
2564 )
2565 .order_asc("id"),
2566 )
2567 .unwrap();
2568
2569 assert_eq!(rows.len(), 1);
2570 assert_eq!(
2571 rows[0].get("name"),
2572 Some(&Value::Text("tea-order".to_owned()))
2573 );
2574 }
2575
2576 #[tokio::test]
2577 async fn memory_data_service_runs_property_to_property_filters() {
2578 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2579 let data_service = MemoryDataService::new(metadata).with_rows(
2580 "Order",
2581 vec![
2582 Record::from([
2583 (String::from("id"), Value::U64(1)),
2584 (String::from("version"), Value::I64(2)),
2585 (String::from("name"), Value::Text(String::from("keep"))),
2586 ]),
2587 Record::from([
2588 (String::from("id"), Value::U64(2)),
2589 (String::from("version"), Value::I64(1)),
2590 (String::from("name"), Value::Text(String::from("skip"))),
2591 ]),
2592 ],
2593 );
2594
2595 let rows = data_service
2596 .fetch_all(
2597 &teaql_core::SelectQuery::new("Order")
2598 .filter(Expr::compare_columns("version", BinaryOp::Gte, "id"))
2599 .order_asc("id"),
2600 )
2601 .unwrap();
2602
2603 assert_eq!(rows.len(), 1);
2604 assert_eq!(rows[0].get("name"), Some(&Value::Text("keep".to_owned())));
2605 }
2606
2607 #[tokio::test]
2608 async fn memory_data_service_supports_mutations_and_optimistic_locking() {
2609 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2610 let data_service = MemoryDataService::new(metadata);
2611
2612 data_service
2613 .insert(
2614 &InsertCommand::new("Order")
2615 .value("id", 10_u64)
2616 .value("version", 1_i64)
2617 .value("name", "draft"),
2618 )
2619 .unwrap();
2620 data_service
2621 .update(
2622 &UpdateCommand::new("Order", 10_u64)
2623 .expected_version(1)
2624 .value("name", "submitted"),
2625 )
2626 .unwrap();
2627
2628 let row = data_service
2629 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2630 .unwrap()
2631 .pop()
2632 .unwrap();
2633 assert_eq!(
2634 row.get("name"),
2635 Some(&Value::Text(String::from("submitted")))
2636 );
2637 assert_eq!(row.get("version"), Some(&Value::I64(2)));
2638
2639 let conflict = data_service
2640 .update(
2641 &UpdateCommand::new("Order", 10_u64)
2642 .expected_version(1)
2643 .value("name", "stale"),
2644 )
2645 .unwrap_err();
2646 assert!(matches!(
2647 conflict,
2648 DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. })
2649 ));
2650
2651 data_service
2652 .delete(&DeleteCommand::new("Order", 10_u64).expected_version(2))
2653 .unwrap();
2654 let row = data_service
2655 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2656 .unwrap()
2657 .pop()
2658 .unwrap();
2659 assert_eq!(row.get("version"), Some(&Value::I64(-3)));
2660
2661 data_service
2662 .recover(&RecoverCommand::new("Order", 10_u64, -3))
2663 .unwrap();
2664 let row = data_service
2665 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
2666 .unwrap()
2667 .pop()
2668 .unwrap();
2669 assert_eq!(row.get("version"), Some(&Value::I64(4)));
2670 }
2671
2672 #[tokio::test]
2673 async fn user_context_reports_missing_schema_provider() {
2674 let err = UserContext::new().ensure_schema().await.unwrap_err();
2675 assert!(
2676 matches!(err, RuntimeError::Schema(message) if message == "missing schema provider")
2677 );
2678 }
2679
2680 #[tokio::test]
2681 async fn user_context_stores_and_exposes_user_identifier() {
2682 let mut context = UserContext::new();
2683 let pid = std::process::id();
2684 let thread_id_str = format!("{:?}", std::thread::current().id());
2685 let numeric_thread_id = thread_id_str
2686 .strip_prefix("ThreadId(")
2687 .and_then(|s| s.strip_suffix(")"))
2688 .unwrap_or(&thread_id_str);
2689 let os_user = std::env::var("USER")
2690 .or_else(|_| std::env::var("USERNAME"))
2691 .unwrap_or_else(|_| "main".to_owned());
2692 let expected_default = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
2693 assert_eq!(context.user_identifier(), Some(expected_default.as_str()));
2694
2695 context.set_user_identifier("user-123");
2696 assert_eq!(context.user_identifier(), Some("user-123"));
2697
2698 let ctx2 = UserContext::new().with_user_identifier("user-456");
2699 assert_eq!(ctx2.user_identifier(), Some("user-456"));
2700
2701 let mut ctx3 = UserContext::new();
2702 ctx3.set_user_identifier_option(Some("user-789".to_owned()));
2703 assert_eq!(ctx3.user_identifier(), Some("user-789"));
2704 ctx3.set_user_identifier_option(None);
2705 assert_eq!(ctx3.user_identifier(), None);
2706
2707 let ctx4 = UserContext::new().with_user_identifier_option(Some("user-abc".to_owned()));
2708 assert_eq!(ctx4.user_identifier(), Some("user-abc"));
2709 }
2710
2711 #[test]
2712 fn local_lock_enforces_ownership_timeout_and_lease_expiry() {
2713 let first = UserContext::new();
2714 let second = UserContext::new();
2715 let key = format!("local-lock-{:?}", std::time::SystemTime::now());
2716
2717 assert!(first.try_local_lock(&key, 0, 50));
2718 assert!(!second.try_local_lock(&key, 0, 50));
2719 second.unlock_local(&key);
2720 assert!(!second.try_local_lock(&key, 0, 50));
2721 std::thread::sleep(std::time::Duration::from_millis(60));
2722 assert!(second.try_local_lock(&key, 0, 50));
2723 second.unlock_local(&key);
2724 assert!(first.try_local_lock(&key, 0, 50));
2725 first.unlock_local(&key);
2726 }
2727
2728 #[derive(Default)]
2729 struct TestRemoteLockProvider {
2730 owners: Mutex<std::collections::HashMap<String, String>>,
2731 }
2732
2733 #[async_trait::async_trait]
2734 impl RemoteLockProvider for TestRemoteLockProvider {
2735 async fn try_remote_lock(
2736 &self,
2737 key: &str,
2738 owner_token: &str,
2739 _timeout_millis: u64,
2740 _expire_millis: u64,
2741 ) -> bool {
2742 let mut owners = self.owners.lock().expect("remote lock state");
2743 if owners.contains_key(key) {
2744 return false;
2745 }
2746 owners.insert(key.to_owned(), owner_token.to_owned());
2747 true
2748 }
2749
2750 async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool {
2751 let mut owners = self.owners.lock().expect("remote lock state");
2752 if owners.get(key).is_some_and(|owner| owner == owner_token) {
2753 owners.remove(key);
2754 return true;
2755 }
2756 false
2757 }
2758 }
2759
2760 #[tokio::test]
2761 async fn remote_lock_delegates_and_preserves_context_ownership() {
2762 let provider: Arc<dyn RemoteLockProvider> = Arc::new(TestRemoteLockProvider::default());
2763 let mut first = UserContext::new();
2764 first.insert_resource(provider.clone());
2765 let mut second = UserContext::new();
2766 second.insert_resource(provider);
2767 let key = format!("remote-lock-{:?}", std::time::SystemTime::now());
2768
2769 assert!(first.try_remote_lock(&key, 0, 1_000).await);
2770 assert!(!second.try_remote_lock(&key, 0, 1_000).await);
2771 assert!(!second.unlock_remote(&key).await);
2772 assert!(!second.try_remote_lock(&key, 0, 1_000).await);
2773 assert!(first.unlock_remote(&key).await);
2774 assert!(second.try_remote_lock(&key, 0, 1_000).await);
2775 assert!(second.unlock_remote(&key).await);
2776
2777 assert!(UserContext::new().try_remote_lock("optional", 0, 0).await);
2778 }
2779}
2780
2781pub use checker::{
2782 CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult, CheckResults, CheckRule, Checker,
2783 CheckerRegistry, InMemoryCheckerRegistry, LocationSegment, ObjectLocation, TypedChecker,
2784 TypedEntityChecker, clear_record_status, mark_record_status,
2785};