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