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