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