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