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