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