1#![allow(warnings)]
2extern crate self as teaql_runtime;
3mod checker;
4mod context;
5mod data_service;
6mod entity_runtime;
7pub mod entity_save;
8mod entity_status;
9mod error;
10mod event;
11pub mod generated_support;
12mod graph;
13mod i18n;
14mod id;
15pub mod inmemory_engine;
16mod language;
17pub mod log_formatter;
18mod memory;
19mod registry;
20mod telemetry;
21#[cfg(feature = "opentelemetry")]
22mod telemetry_opentelemetry;
23
24pub use context::{
25 ContextEntityRef, ContextRootError, ContinuousPageCursor, ContinuousPageCursorStore, DataStore,
26 IdSetStore, InMemoryContinuousPageCursorStore, InMemoryDataStore, InMemoryIdSetStore,
27 InfoLogEntry, LogPayload, RemoteLockProvider, RetainedIdSet, SchemaProvider, SqlLogEntry,
28 SqlLogOperation, SqlLogOptions, UnifiedLogBuffer, UnifiedLogEntry, UserContext,
29};
30pub use data_service::{
31 AggregationCacheBackend, EntityDataService, GraphTransactionBoundary, InMemoryAggregationCache,
32 RelationLoadPlan,
33};
34pub use entity_runtime::{
35 ChangeSetStack, EntityChangeSet, EntityGraphBuilder, EntityKey, EntityRoot, LedgerEntity,
36 RootContext,
37};
38pub use entity_save::{AuditedSaveExt, graph_node_from_entity, save_audited_ledger_entity};
39pub use entity_status::{EntityAction, EntityStatus};
40pub use error::{ContextError, DataServiceError, RuntimeError};
41pub use event::{
42 EntityPropertyChange, InMemoryRawAuditEventSink, RawAuditEvent, RawAuditEventKind,
43 RawAuditEventSink, SafeAuditEvent, SafeAuditEventSink, SafeAuditField,
44};
45pub use generated_support::*;
46pub use graph::{
47 EntityValues, GraphMutationBatch, GraphMutationKind, GraphMutationPlan, GraphMutationPlanItem,
48 GraphNode, GraphOperation, ScopedCommentNode, TraceScopeToken, sorted_update_fields,
49};
50pub use i18n::I18nCatalog;
51pub(crate) use id::local_id_generator;
52pub use id::{
53 AtomicCounterIdGenerator, InternalIdGenerator, SnowflakeIdGenerator, canonical_id_space_entity,
54};
55pub use inmemory_engine::{ExprEvaluator, InMemoryQueryEngine};
56pub use language::{
57 BuiltinTranslator, Language, Locale, MessageTranslator, translate_check_result,
58 translate_location,
59};
60pub(crate) use memory::MemoryDataService;
61pub use registry::{
62 EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityRegistry,
63 InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityGraphDecoderRegistry,
64 InMemoryEntityRegistry, InMemoryMetadataStore, MetadataStore, RequestPolicy, RuntimeModule,
65};
66pub use telemetry::{
67 FailOpenRuntimeTelemetryPropagationContext, FailOpenRuntimeTelemetryScope,
68 NoopRuntimeTelemetry, RuntimeAttributeValue, RuntimeOperation, RuntimeTelemetry,
69 RuntimeTelemetryPropagationContext, RuntimeTelemetryScope, extract_runtime_context,
70 runtime_error_category, start_runtime_operation,
71};
72#[cfg(feature = "opentelemetry")]
73pub use telemetry_opentelemetry::OpenTelemetryRuntimeTelemetry;
74
75#[cfg(test)]
76mod tests {
77 use std::collections::{BTreeMap, VecDeque};
78 use std::sync::{Arc, Mutex};
79
80 use super::{
81 AggregationCacheBackend, CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult,
82 CheckResults, CheckRule, Checker, DataServiceError, EntityDataServiceBehavior, EntityRoot,
83 EntityValues, GraphMutationKind, GraphNode, I18nCatalog, InMemoryAggregationCache,
84 InMemoryCheckerRegistry, InMemoryEntityDataServiceBehaviorRegistry, InMemoryEntityRegistry,
85 InMemoryMetadataStore, InternalIdGenerator, Language, MemoryDataService, MetadataStore,
86 ObjectLocation, RawAuditEvent, RawAuditEventKind, RawAuditEventSink, RemoteLockProvider,
87 RequestPolicy, RuntimeError, RuntimeModule, RuntimeOperation, RuntimeTelemetry,
88 RuntimeTelemetryScope, SafeAuditEvent, SafeAuditEventSink, SqlLogOperation, SqlLogOptions,
89 TypedChecker, TypedEntityChecker, UserContext, translate_check_result,
90 };
91 use crate::data_service::RuntimeDataService;
92 use teaql_core::{
93 Aggregate, AggregateFunction, BinaryOp, DataType, Decimal, DeleteCommand, Entity,
94 EntityDescriptor, EntityError, Expr, GeneratedValues, InsertCommand, OrderBy,
95 PropertyDescriptor, Record, RecoverCommand, RelationAggregate, SelectQuery, TeaqlEntity,
96 UpdateCommand, Value,
97 };
98 use teaql_data_service::{
99 DataServiceCapabilities, DataServiceExecutor, DataServiceOperation, ExecutionMetadata,
100 MutationExecutor, MutationRequest, MutationResult, QueryExecutor, QueryRequest,
101 QueryResult,
102 };
103 use teaql_macros::TeaqlEntity as DeriveTeaqlEntity;
104 use teaql_sql::{
105 CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, quote_identifier_if_needed,
106 };
107
108 const ORDER_DEFAULT_PROJECTION: &str = "id, version, name";
109
110 #[derive(Debug, Default, Clone, Copy)]
111 struct PostgresDialect;
112
113 impl SqlDialect for PostgresDialect {
114 fn kind(&self) -> DatabaseKind {
115 DatabaseKind::PostgreSql
116 }
117
118 fn quote_ident(&self, ident: &str) -> String {
119 quote_identifier_if_needed(ident, '"')
120 }
121
122 fn placeholder(&self, index: usize) -> String {
123 format!("${index}")
124 }
125
126 fn schema_type_sql(
127 &self,
128 data_type: DataType,
129 _property: &PropertyDescriptor,
130 ) -> Result<&'static str, SqlCompileError> {
131 match data_type {
132 DataType::Bool => Ok("BOOLEAN"),
133 DataType::I64 | DataType::U64 => Ok("BIGINT"),
134 DataType::F64 => Ok("DOUBLE PRECISION"),
135 DataType::Decimal => Ok("NUMERIC"),
136 DataType::Text => Ok("VARCHAR(255)"),
137 DataType::LargeText => Ok("TEXT"),
138 DataType::Json => Ok("JSONB"),
139 DataType::Date => Ok("DATE"),
140 DataType::Timestamp => Ok("TIMESTAMPTZ"),
141 }
142 }
143 }
144
145 fn entity() -> EntityDescriptor {
146 EntityDescriptor::new("Order")
147 .table_name("orders")
148 .property(
149 PropertyDescriptor::new("id", DataType::U64)
150 .column_name("id")
151 .id()
152 .not_null(),
153 )
154 .property(
155 PropertyDescriptor::new("version", DataType::I64)
156 .column_name("version")
157 .version()
158 .not_null(),
159 )
160 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
161 .relation(
162 teaql_core::RelationDescriptor::new("lines", "OrderLine")
163 .local_key("id")
164 .foreign_key("order_id")
165 .many(),
166 )
167 }
168
169 fn line_entity() -> EntityDescriptor {
170 EntityDescriptor::new("OrderLine")
171 .table_name("orderline")
172 .property(
173 PropertyDescriptor::new("id", DataType::U64)
174 .column_name("id")
175 .id()
176 .not_null(),
177 )
178 .property(
179 PropertyDescriptor::new("version", DataType::I64)
180 .column_name("version")
181 .version(),
182 )
183 .property(
184 PropertyDescriptor::new("order_id", DataType::U64)
185 .column_name("order_id")
186 .not_null(),
187 )
188 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
189 .property(
190 PropertyDescriptor::new("product_id", DataType::U64)
191 .column_name("product_id")
192 .not_null(),
193 )
194 .relation(
195 teaql_core::RelationDescriptor::new("product", "Product")
196 .local_key("product_id")
197 .foreign_key("id"),
198 )
199 }
200
201 fn product_entity() -> EntityDescriptor {
202 EntityDescriptor::new("Product")
203 .table_name("product")
204 .property(
205 PropertyDescriptor::new("id", DataType::U64)
206 .column_name("id")
207 .id()
208 .not_null(),
209 )
210 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
211 }
212
213 #[derive(Debug, Default)]
214 struct StubExecutor {
215 affected: u64,
216 rows: Vec<Record>,
217 }
218
219 #[derive(Debug, Default)]
220 struct QueueExecutor {
221 affected: u64,
222 rows: Mutex<VecDeque<Vec<Record>>>,
223 queries: Mutex<Vec<String>>,
224 }
225
226 #[derive(Debug, Default)]
227 struct IdSetQueueExecutor {
228 rows: Mutex<VecDeque<Vec<Record>>>,
229 queries: Mutex<Vec<SelectQuery>>,
230 }
231
232 #[derive(Debug, Clone, Default)]
233 struct ConcurrentIdSetExecutor {
234 id_queries: Arc<std::sync::atomic::AtomicUsize>,
235 }
236
237 struct UnavailableIdSetStore;
238
239 #[async_trait::async_trait]
240 impl crate::IdSetStore for UnavailableIdSetStore {
241 async fn get(&self, _query_key: &str) -> Result<Option<crate::RetainedIdSet>, String> {
242 Err("unavailable".to_owned())
243 }
244
245 async fn put(&self, _id_set: crate::RetainedIdSet) -> Result<(), String> {
246 Err("unavailable".to_owned())
247 }
248
249 async fn invalidate(&self, _query_key: &str) -> Result<(), String> {
250 Err("unavailable".to_owned())
251 }
252 }
253
254 #[derive(Debug, Default)]
255 struct CapturingQueryExecutor {
256 rows: Vec<Record>,
257 queries: Mutex<Vec<SelectQuery>>,
258 }
259
260 struct OrderBehavior;
261
262 #[allow(dead_code)]
263 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
264 #[teaql(entity = "CatalogProduct", table = "catalog_product")]
265 struct CatalogProductRow {
266 #[teaql(id)]
267 id: u64,
268 name: String,
269 }
270
271 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
272 #[teaql(entity = "OrderAggregate", table = "orders")]
273 struct OrderAggregateDynamic {
274 #[teaql(id)]
275 id: u64,
276 #[teaql(dynamic)]
277 dynamic: BTreeMap<String, Value>,
278 }
279
280 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
281 #[teaql(entity = "Product", table = "product")]
282 struct ProductEntityRow {
283 #[teaql(id)]
284 id: u64,
285 name: String,
286 }
287
288 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
289 #[teaql(entity = "OrderLine", table = "orderline")]
290 struct OrderLineEntityRow {
291 #[teaql(id)]
292 id: u64,
293 #[teaql(column = "order_id")]
294 order_id: u64,
295 name: String,
296 #[teaql(column = "product_id")]
297 product_id: u64,
298 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
299 product: Option<ProductEntityRow>,
300 }
301
302 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
303 #[teaql(entity = "OrderLine", table = "orderline")]
304 struct ProductLineEntityRow {
305 #[teaql(id)]
306 id: u64,
307 #[teaql(column = "order_id")]
308 order_id: u64,
309 name: String,
310 #[teaql(column = "product_id")]
311 product_id: u64,
312 }
313
314 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
315 #[teaql(entity = "Product", table = "product")]
316 struct ProductWithLinesEntityRow {
317 #[teaql(id)]
318 id: u64,
319 name: String,
320 #[teaql(relation(
321 target = "OrderLine",
322 local_key = "id",
323 foreign_key = "product_id",
324 many
325 ))]
326 lines: teaql_core::SmartList<ProductLineEntityRow>,
327 }
328
329 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
330 #[teaql(entity = "OrderLine", table = "orderline")]
331 struct OrderLineWithProductEntityRow {
332 #[teaql(id)]
333 id: u64,
334 #[teaql(column = "order_id")]
335 order_id: u64,
336 name: String,
337 #[teaql(column = "product_id")]
338 product_id: u64,
339 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
340 product: Option<ProductWithLinesEntityRow>,
341 }
342
343 #[derive(Debug, DeriveTeaqlEntity)]
344 #[teaql(entity = "FlatVendor", table = "flat_vendor")]
345 struct FlatVendorRow {
346 #[teaql(id)]
347 id: u64,
348 name: String,
349 #[teaql(skip)]
350 root: EntityRoot,
351 }
352
353 #[derive(Debug, DeriveTeaqlEntity)]
354 #[teaql(entity = "FlatTrip", table = "flat_trip")]
355 struct FlatTripRow {
356 #[teaql(id)]
357 id: u64,
358 vendor_id: u64,
359 #[teaql(relation(target = "FlatVendor", local_key = "vendor_id", foreign_key = "id"))]
360 vendor: Option<FlatVendorRow>,
361 #[teaql(skip)]
362 root: EntityRoot,
363 }
364
365 impl FlatTripRow {
366 fn vendor(&self) -> Option<&FlatVendorRow> {
367 self.vendor
368 .as_ref()
369 .or_else(|| self.root.resolve_entity::<FlatVendorRow>(self.vendor_id))
370 }
371 }
372
373 #[derive(Clone, Debug, DeriveTeaqlEntity)]
374 #[teaql(entity = "FlatFleet", table = "flat_fleet")]
375 struct FlatFleetRow {
376 #[teaql(id)]
377 id: u64,
378 #[teaql(relation(
379 target = "FlatFleetTrip",
380 local_key = "id",
381 foreign_key = "fleet_id",
382 many
383 ))]
384 trip_list: teaql_core::SmartList<FlatFleetTripRow>,
385 #[teaql(skip)]
386 root: EntityRoot,
387 }
388
389 impl FlatFleetRow {
390 fn trip_list(&self) -> &teaql_core::SmartList<FlatFleetTripRow> {
391 if self.trip_list.is_loaded {
392 &self.trip_list
393 } else {
394 self.root
395 .resolve_relation_list(Self::ENTITY_NAME, self.id, "trip_list")
396 .unwrap_or(&self.trip_list)
397 }
398 }
399
400 fn trip_list_mut(&mut self) -> &mut teaql_core::SmartList<FlatFleetTripRow> {
401 if !self.trip_list.is_loaded {
402 if let Some(loaded) = self
403 .root
404 .resolve_relation_list(Self::ENTITY_NAME, self.id, "trip_list")
405 .cloned()
406 {
407 self.trip_list = loaded;
408 }
409 }
410 &mut self.trip_list
411 }
412 }
413
414 #[derive(Clone, Debug, DeriveTeaqlEntity)]
415 #[teaql(entity = "FlatFleetTrip", table = "flat_fleet_trip")]
416 struct FlatFleetTripRow {
417 #[teaql(id)]
418 id: u64,
419 fleet_id: u64,
420 name: String,
421 #[teaql(skip)]
422 root: EntityRoot,
423 }
424
425 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
426 #[teaql(entity = "Order", table = "orders")]
427 struct OrderAggregateRow {
428 #[teaql(id)]
429 id: u64,
430 #[teaql(version)]
431 version: i64,
432 name: String,
433 #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
434 lines: teaql_core::SmartList<OrderLineEntityRow>,
435 }
436
437 #[derive(Debug, Clone, PartialEq, DeriveTeaqlEntity)]
438 #[teaql(entity = "Order", table = "orders")]
439 struct Order {
440 #[teaql(id)]
441 id: u64,
442 #[teaql(version)]
443 version: i64,
444 name: String,
445 }
446
447 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
448 #[teaql(entity = "Product", table = "product")]
449 struct TypedGraphProduct {
450 #[teaql(id)]
451 id: u64,
452 name: String,
453 }
454
455 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
456 #[teaql(entity = "OrderLine", table = "orderline")]
457 struct TypedGraphLine {
458 #[teaql(id)]
459 id: u64,
460 #[teaql(column = "order_id")]
461 order_id: Option<u64>,
462 name: String,
463 #[teaql(column = "product_id")]
464 product_id: Option<u64>,
465 #[teaql(relation(target = "Product", local_key = "product_id", foreign_key = "id"))]
466 product: Option<TypedGraphProduct>,
467 }
468
469 #[derive(Debug, PartialEq, DeriveTeaqlEntity)]
470 #[teaql(entity = "Order", table = "orders")]
471 struct TypedGraphOrder {
472 #[teaql(id)]
473 id: u64,
474 #[teaql(version)]
475 version: i64,
476 name: String,
477 #[teaql(relation(target = "OrderLine", local_key = "id", foreign_key = "order_id", many))]
478 lines: teaql_core::SmartList<TypedGraphLine>,
479 }
480
481 #[derive(Debug, PartialEq, Eq)]
482 struct OrderEntity {
483 id: u64,
484 version: i64,
485 name: String,
486 }
487
488 impl teaql_core::TeaqlEntity for OrderEntity {
489 const ENTITY_NAME: &'static str = "Order";
490
491 fn entity_descriptor() -> EntityDescriptor {
492 entity()
493 }
494 }
495
496 impl Entity for OrderEntity {
497 fn from_compact_row(row: teaql_core::CompactRow) -> Result<Self, EntityError> {
498 let record = row.into_map();
499 let id = match record.get("id") {
500 Some(Value::U64(v)) => *v,
501 Some(Value::I64(v)) if *v >= 0 => *v as u64,
502 other => {
503 return Err(EntityError::new(
504 "Order",
505 format!("invalid id field: {other:?}"),
506 ));
507 }
508 };
509 let version = match record.get("version") {
510 Some(Value::I64(v)) => *v,
511 other => {
512 return Err(EntityError::new(
513 "Order",
514 format!("invalid version field: {other:?}"),
515 ));
516 }
517 };
518 let name = match record.get("name") {
519 Some(Value::Text(v)) => v.clone(),
520 other => {
521 return Err(EntityError::new(
522 "Order",
523 format!("invalid name field: {other:?}"),
524 ));
525 }
526 };
527 Ok(Self { id, version, name })
528 }
529
530 fn into_values(self) -> teaql_core::MutationValues {
531 Record::from([
532 (String::from("id"), Value::U64(self.id)),
533 (String::from("version"), Value::I64(self.version)),
534 (String::from("name"), Value::Text(self.name)),
535 ])
536 .into()
537 }
538 }
539
540 #[derive(Debug)]
541 struct StubError;
542
543 struct RecordingRuntimeTelemetry(Arc<Mutex<Vec<String>>>);
544
545 impl RuntimeTelemetry for RecordingRuntimeTelemetry {
546 fn start(&self, operation: RuntimeOperation) -> Box<dyn RuntimeTelemetryScope> {
547 self.0
548 .lock()
549 .unwrap()
550 .push(format!("start:{}", operation.family));
551 Box::new(RecordingRuntimeTelemetryScope(self.0.clone()))
552 }
553 }
554
555 struct RecordingRuntimeTelemetryScope(Arc<Mutex<Vec<String>>>);
556
557 impl RuntimeTelemetryScope for RecordingRuntimeTelemetryScope {
558 fn success(&mut self, _attributes: BTreeMap<String, crate::RuntimeAttributeValue>) {
559 self.0.lock().unwrap().push("success".to_owned());
560 }
561
562 fn failure(&mut self, _error_type: &str) {
563 self.0.lock().unwrap().push("failure".to_owned());
564 }
565 }
566
567 impl std::fmt::Display for StubError {
568 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569 write!(f, "stub error")
570 }
571 }
572
573 impl std::error::Error for StubError {}
574
575 impl DataServiceExecutor for StubExecutor {
576 type Error = StubError;
577
578 fn capabilities(&self) -> DataServiceCapabilities {
579 DataServiceCapabilities::default()
580 }
581 }
582
583 impl QueryExecutor for StubExecutor {
584 async fn query(&self, _request: QueryRequest) -> Result<QueryResult, Self::Error> {
585 Ok(QueryResult {
586 rows: self
587 .rows
588 .clone()
589 .into_iter()
590 .map(teaql_core::CompactRow::from_map)
591 .collect(),
592 metadata: ExecutionMetadata {
593 debug_query: None,
594 backend: "stub".to_owned(),
595 operation: DataServiceOperation::Query,
596 started_at: std::time::SystemTime::now(),
597 ended_at: std::time::SystemTime::now(),
598 affected_rows: None,
599 result_count: Some(self.rows.len()),
600 trace_chain: Vec::new(),
601 comment: None,
602 backend_request_id: None,
603 parameterized_query: None,
604 params: Vec::new(),
605 },
606 })
607 }
608 }
609
610 impl MutationExecutor for StubExecutor {
611 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
612 Ok(MutationResult {
613 affected_rows: self.affected,
614 generated_values: GeneratedValues::new(),
615 persisted_snapshot: None,
616 metadata: ExecutionMetadata {
617 debug_query: None,
618 backend: "stub".to_owned(),
619 operation: DataServiceOperation::Update,
620 started_at: std::time::SystemTime::now(),
621 ended_at: std::time::SystemTime::now(),
622 affected_rows: Some(self.affected),
623 result_count: None,
624 trace_chain: Vec::new(),
625 comment: None,
626 backend_request_id: None,
627 parameterized_query: None,
628 params: Vec::new(),
629 },
630 })
631 }
632 }
633
634 impl DataServiceExecutor for CapturingQueryExecutor {
635 type Error = StubError;
636
637 fn capabilities(&self) -> DataServiceCapabilities {
638 DataServiceCapabilities::default()
639 }
640 }
641
642 impl QueryExecutor for CapturingQueryExecutor {
643 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
644 self.queries.lock().unwrap().push(request.query);
645 Ok(QueryResult {
646 rows: self
647 .rows
648 .clone()
649 .into_iter()
650 .map(teaql_core::CompactRow::from_map)
651 .collect(),
652 metadata: ExecutionMetadata {
653 debug_query: None,
654 backend: "capture".to_owned(),
655 operation: DataServiceOperation::Query,
656 started_at: std::time::SystemTime::now(),
657 ended_at: std::time::SystemTime::now(),
658 affected_rows: None,
659 result_count: Some(self.rows.len()),
660 trace_chain: Vec::new(),
661 comment: None,
662 backend_request_id: None,
663 parameterized_query: None,
664 params: Vec::new(),
665 },
666 })
667 }
668 }
669
670 impl MutationExecutor for CapturingQueryExecutor {
671 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
672 unreachable!("relation query test does not mutate")
673 }
674 }
675
676 impl DataServiceExecutor for QueueExecutor {
677 type Error = StubError;
678
679 fn capabilities(&self) -> DataServiceCapabilities {
680 DataServiceCapabilities::default()
681 }
682 }
683
684 impl QueryExecutor for QueueExecutor {
685 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
686 let sql_approx = format!("SELECT ... FROM {} ...", request.query.entity);
687 self.queries.lock().unwrap().push(sql_approx);
688 Ok(QueryResult {
689 rows: self
690 .rows
691 .lock()
692 .unwrap()
693 .pop_front()
694 .unwrap_or_default()
695 .into_iter()
696 .map(teaql_core::CompactRow::from_map)
697 .collect(),
698 metadata: ExecutionMetadata {
699 debug_query: None,
700 backend: "queue".to_owned(),
701 operation: DataServiceOperation::Query,
702 started_at: std::time::SystemTime::now(),
703 ended_at: std::time::SystemTime::now(),
704 affected_rows: None,
705 result_count: Some(0),
706 trace_chain: Vec::new(),
707 comment: None,
708 backend_request_id: None,
709 parameterized_query: None,
710 params: Vec::new(),
711 },
712 })
713 }
714 }
715
716 impl MutationExecutor for QueueExecutor {
717 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
718 Ok(MutationResult {
719 affected_rows: self.affected,
720 generated_values: GeneratedValues::new(),
721 persisted_snapshot: None,
722 metadata: ExecutionMetadata {
723 debug_query: None,
724 backend: "queue".to_owned(),
725 operation: DataServiceOperation::Update,
726 started_at: std::time::SystemTime::now(),
727 ended_at: std::time::SystemTime::now(),
728 affected_rows: Some(self.affected),
729 result_count: None,
730 trace_chain: Vec::new(),
731 comment: None,
732 backend_request_id: None,
733 parameterized_query: None,
734 params: Vec::new(),
735 },
736 })
737 }
738 }
739
740 impl DataServiceExecutor for IdSetQueueExecutor {
741 type Error = StubError;
742
743 fn capabilities(&self) -> DataServiceCapabilities {
744 DataServiceCapabilities::default()
745 }
746 }
747
748 impl QueryExecutor for IdSetQueueExecutor {
749 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
750 self.queries.lock().unwrap().push(request.query);
751 let rows = self.rows.lock().unwrap().pop_front().unwrap_or_default();
752 Ok(QueryResult {
753 rows: rows
754 .into_iter()
755 .map(teaql_core::CompactRow::from_map)
756 .collect(),
757 metadata: ExecutionMetadata {
758 debug_query: None,
759 backend: "id-set-queue".to_owned(),
760 operation: DataServiceOperation::Query,
761 started_at: std::time::SystemTime::now(),
762 ended_at: std::time::SystemTime::now(),
763 affected_rows: None,
764 result_count: None,
765 trace_chain: Vec::new(),
766 comment: None,
767 backend_request_id: None,
768 parameterized_query: None,
769 params: Vec::new(),
770 },
771 })
772 }
773 }
774
775 impl MutationExecutor for IdSetQueueExecutor {
776 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
777 unreachable!("ID set query test does not mutate")
778 }
779 }
780
781 impl DataServiceExecutor for ConcurrentIdSetExecutor {
782 type Error = StubError;
783
784 fn capabilities(&self) -> DataServiceCapabilities {
785 DataServiceCapabilities::default()
786 }
787 }
788
789 impl QueryExecutor for ConcurrentIdSetExecutor {
790 async fn query(&self, request: QueryRequest) -> Result<QueryResult, Self::Error> {
791 let id_only = request.query.projection == ["id"];
792 let rows = if id_only {
793 self.id_queries
794 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
795 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
796 vec![
797 Record::from([(String::from("id"), Value::U64(1))]),
798 Record::from([(String::from("id"), Value::U64(2))]),
799 ]
800 } else {
801 vec![Record::from([
802 (String::from("id"), Value::U64(1)),
803 (String::from("version"), Value::I64(1)),
804 (String::from("name"), Value::Text("order-1".to_owned())),
805 ])]
806 };
807 Ok(QueryResult {
808 rows: rows
809 .into_iter()
810 .map(teaql_core::CompactRow::from_map)
811 .collect(),
812 metadata: ExecutionMetadata {
813 debug_query: None,
814 backend: "concurrent-id-set".to_owned(),
815 operation: DataServiceOperation::Query,
816 started_at: std::time::SystemTime::now(),
817 ended_at: std::time::SystemTime::now(),
818 affected_rows: None,
819 result_count: None,
820 trace_chain: Vec::new(),
821 comment: None,
822 backend_request_id: None,
823 parameterized_query: None,
824 params: Vec::new(),
825 },
826 })
827 }
828 }
829
830 impl MutationExecutor for ConcurrentIdSetExecutor {
831 async fn mutate(&self, _request: MutationRequest) -> Result<MutationResult, Self::Error> {
832 unreachable!("ID set concurrency test does not mutate")
833 }
834 }
835
836 impl EntityDataServiceBehavior for OrderBehavior {
837 fn before_select(
838 &self,
839 _ctx: &UserContext,
840 query: &mut teaql_core::SelectQuery,
841 ) -> Result<(), RuntimeError> {
842 query.filter = Some(Expr::eq("version", 1_i64));
843 Ok(())
844 }
845
846 fn before_insert(
847 &self,
848 _ctx: &UserContext,
849 command: &mut InsertCommand,
850 ) -> Result<(), RuntimeError> {
851 command
852 .values
853 .entry("version".to_owned())
854 .or_insert(Value::I64(1));
855 Ok(())
856 }
857
858 fn relation_loads(&self, _ctx: &UserContext) -> Vec<String> {
859 vec!["lines".to_owned()]
860 }
861 }
862
863 struct ContextAwareOrderBehavior;
864 struct TenantRequestPolicy;
865 struct OrderChecker;
866 struct TypedOrderChecker;
867 #[derive(Clone)]
868 struct RecordingEventSink {
869 events: Arc<Mutex<Vec<RawAuditEvent>>>,
870 }
871 #[derive(Clone)]
872 struct RecordingSafeEventSink {
873 events: Arc<Mutex<Vec<SafeAuditEvent>>>,
874 }
875
876 impl EntityDataServiceBehavior for ContextAwareOrderBehavior {
877 fn before_insert(
878 &self,
879 context: &UserContext,
880 command: &mut InsertCommand,
881 ) -> Result<(), RuntimeError> {
882 let tenant = context
883 .get_named_resource::<String>("tenant")
884 .cloned()
885 .ok_or_else(|| RuntimeError::Behavior("missing tenant resource".to_owned()))?;
886 let version = *context
887 .get_named_resource::<i64>("initial_version")
888 .ok_or_else(|| {
889 RuntimeError::Behavior("missing initial_version resource".to_owned())
890 })?;
891 let trace_id = match context.local("trace_id") {
892 Some(Value::Text(value)) => value.clone(),
893 other => {
894 return Err(RuntimeError::Behavior(format!(
895 "missing trace_id local, got {other:?}"
896 )));
897 }
898 };
899
900 command
901 .values
902 .entry("name".to_owned())
903 .or_insert(Value::Text(format!("{tenant}:{trace_id}")));
904 command
905 .values
906 .entry("version".to_owned())
907 .or_insert(Value::I64(version));
908 Ok(())
909 }
910 }
911
912 impl RequestPolicy for TenantRequestPolicy {
913 fn enforce_select(
914 &self,
915 context: &UserContext,
916 query: &mut SelectQuery,
917 ) -> Result<(), RuntimeError> {
918 if query.entity == "Order" {
919 let tenant_id = context
920 .get_named_resource::<u64>("tenant_id")
921 .copied()
922 .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
923 query.filter = Some(match query.filter.take() {
924 Some(filter) => filter.and_expr(Expr::eq("id", tenant_id)),
925 None => Expr::eq("id", tenant_id),
926 });
927 }
928 Ok(())
929 }
930
931 fn enforce_insert(
932 &self,
933 context: &UserContext,
934 command: &mut InsertCommand,
935 ) -> Result<(), RuntimeError> {
936 if command.entity == "Order" {
937 let tenant_id = context
938 .get_named_resource::<u64>("tenant_id")
939 .copied()
940 .ok_or_else(|| RuntimeError::Policy("missing tenant_id".to_owned()))?;
941 command
942 .values
943 .insert("version".to_owned(), Value::I64(tenant_id as i64));
944 }
945 Ok(())
946 }
947 }
948
949 impl Checker for OrderChecker {
950 fn entity(&self) -> &str {
951 "Order"
952 }
953
954 fn check_and_fix(
955 &self,
956 _ctx: &UserContext,
957 values: &mut EntityValues,
958 location: &ObjectLocation,
959 results: &mut CheckResults,
960 ) {
961 let status = CheckObjectStatus::from_values(values);
962 if status.is_create() {
963 self.required(values, "name", location, results);
964 values.entry("version".to_owned()).or_insert(Value::I64(1));
965 }
966 if status.is_update()
967 && values.get("name") == Some(&Value::Text("graph-update".to_owned()))
968 {
969 values.insert(
970 "name".to_owned(),
971 Value::Text("graph-update-checked".to_owned()),
972 );
973 }
974 self.min_string_length(values, "name", 3, location, results);
975 }
976 }
977
978 impl TypedChecker<Order> for TypedOrderChecker {
979 fn check_and_fix_typed(
980 &self,
981 _ctx: &UserContext,
982 entity: &mut Order,
983 status: CheckObjectStatus,
984 location: &ObjectLocation,
985 results: &mut CheckResults,
986 ) {
987 if status.is_create() {
988 if entity.name.is_empty() {
989 results.push(CheckResult::required(location.clone().member("name")));
990 }
991 }
992 if entity.name.chars().count() < 3 {
993 results.push(CheckResult::min_str(
994 location.clone().member("name"),
995 3,
996 entity.name.clone(),
997 ));
998 }
999 if entity.name == "fix" {
1000 entity.name = "fixed".to_owned();
1001 }
1002 }
1003 }
1004
1005 impl RawAuditEventSink for RecordingEventSink {
1006 fn on_event(&self, _ctx: &UserContext, event: &RawAuditEvent) -> Result<(), RuntimeError> {
1007 self.events.lock().unwrap().push(event.clone());
1008 Ok(())
1009 }
1010 }
1011
1012 impl SafeAuditEventSink for RecordingSafeEventSink {
1013 fn on_safe_event(
1014 &self,
1015 _ctx: &UserContext,
1016 event: &SafeAuditEvent,
1017 ) -> Result<(), RuntimeError> {
1018 self.events.lock().unwrap().push(event.clone());
1019 Ok(())
1020 }
1021 }
1022
1023 struct FixedIdGenerator(u64);
1024
1025 impl InternalIdGenerator for FixedIdGenerator {
1026 fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
1027 Ok(self.0)
1028 }
1029 }
1030
1031 struct SequentialIdGenerator {
1032 next: Mutex<u64>,
1033 }
1034
1035 impl SequentialIdGenerator {
1036 fn new(next: u64) -> Self {
1037 Self {
1038 next: Mutex::new(next),
1039 }
1040 }
1041 }
1042
1043 impl InternalIdGenerator for SequentialIdGenerator {
1044 fn generate_id(&self, _entity: &str) -> Result<u64, RuntimeError> {
1045 let mut next = self
1046 .next
1047 .lock()
1048 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?;
1049 let id = *next;
1050 *next += 1;
1051 Ok(id)
1052 }
1053 }
1054
1055 #[tokio::test]
1056 async fn metadata_store_registers_entities() {
1057 let store = InMemoryMetadataStore::new().with_entity(entity());
1058 assert!(store.entity("Order").is_some());
1059 }
1060
1061 #[tokio::test]
1062 async fn runtime_module_registers_descriptor_into_context() {
1063 let context = UserContext::new().with_module(RuntimeModule::new().descriptor(entity()));
1064 assert!(context.entity("Order").is_some());
1065 assert!(context.has_entity_data_service("Order"));
1066 }
1067
1068 #[tokio::test]
1069 async fn runtime_module_registers_derived_entity_and_behavior() {
1070 let context = UserContext::new().with_module(
1071 RuntimeModule::new().entity_with_behavior::<CatalogProductRow, _>(OrderBehavior),
1072 );
1073 assert!(context.entity("CatalogProduct").is_some());
1074 assert!(context.has_entity_data_service("CatalogProduct"));
1075 assert!(
1076 context
1077 .entity_data_service_behavior("CatalogProduct")
1078 .is_some()
1079 );
1080 }
1081
1082 #[tokio::test]
1083 async fn module_macro_registers_multiple_entities() {
1084 let context = UserContext::new().with_module(crate::module!(CatalogProductRow));
1085 assert!(context.entity("CatalogProduct").is_some());
1086 assert!(context.has_entity_data_service("CatalogProduct"));
1087 }
1088
1089 #[tokio::test]
1090 async fn module_macro_registers_entity_behavior_pairs() {
1091 let context =
1092 UserContext::new().with_module(crate::module!(CatalogProductRow => OrderBehavior));
1093 assert!(context.entity("CatalogProduct").is_some());
1094 assert!(
1095 context
1096 .entity_data_service_behavior("CatalogProduct")
1097 .is_some()
1098 );
1099 }
1100
1101 #[tokio::test]
1102 async fn data_service_returns_optimistic_lock_conflict() {
1103 let store = InMemoryMetadataStore::new().with_entity(entity());
1104 let executor = StubExecutor {
1105 affected: 0,
1106 rows: Vec::new(),
1107 };
1108 let repo = RuntimeDataService::new(&store, &executor);
1109
1110 let err = repo
1111 .update(
1112 &UpdateCommand::new("Order", 1_u64)
1113 .expected_version(3)
1114 .value("name", "next"),
1115 )
1116 .await
1117 .unwrap_err();
1118
1119 match err {
1120 DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. }) => {}
1121 other => panic!("unexpected error: {other}"),
1122 }
1123 }
1124
1125 #[tokio::test]
1126 async fn user_context_indexes_resources_and_locals() {
1127 let mut context =
1128 UserContext::new().with_metadata(InMemoryMetadataStore::new().with_entity(entity()));
1129 context.insert_resource::<u64>(42);
1130 context.insert_named_resource("tenant", String::from("acme"));
1131 context.put_local("trace_id", "req-1");
1132
1133 assert!(context.entity("Order").is_some());
1134 assert_eq!(context.get_resource::<u64>(), Some(&42));
1135 assert_eq!(
1136 context.get_named_resource::<String>("tenant"),
1137 Some(&String::from("acme"))
1138 );
1139 assert_eq!(
1140 context.local("trace_id"),
1141 Some(&Value::Text("req-1".to_owned()))
1142 );
1143 }
1144
1145 #[tokio::test]
1146 async fn user_context_builds_context_data_service() {
1147 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
1148 let mut context = UserContext::new()
1149 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1150 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
1151 telemetry_events.clone(),
1152 )));
1153 context.insert_resource(PostgresDialect);
1154 context.insert_resource(StubExecutor {
1155 affected: 1,
1156 rows: Vec::new(),
1157 });
1158
1159 let repo = context.data_service_internal::<StubExecutor>().unwrap();
1160 let affected = repo
1161 .update(
1162 &UpdateCommand::new("Order", 1_u64)
1163 .expected_version(3)
1164 .value("name", "next"),
1165 )
1166 .await
1167 .unwrap();
1168
1169 assert_eq!(affected, 1);
1170 assert_eq!(
1171 telemetry_events.lock().unwrap().as_slice(),
1172 ["start:mutation", "start:provider", "success", "success"]
1173 );
1174 }
1175
1176 #[tokio::test]
1177 async fn user_context_resolves_entity_data_service_by_entity_type() {
1178 let mut context = UserContext::new()
1179 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1180 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1181 context.insert_resource(PostgresDialect);
1182 context.insert_resource(StubExecutor {
1183 affected: 1,
1184 rows: Vec::new(),
1185 });
1186
1187 let repo = context
1188 .entity_data_service::<StubExecutor>("Order")
1189 .unwrap();
1190 assert_eq!(repo.entity(), "Order");
1191 assert_eq!(repo.select().entity, "Order");
1192
1193 let affected = repo
1194 .insert_internal(
1195 &repo
1196 .insert_command()
1197 .value("id", 1_u64)
1198 .value("version", 1_i64)
1199 .value("name", "n"),
1200 )
1201 .await
1202 .unwrap();
1203 assert_eq!(affected, 1);
1204 }
1205
1206 #[tokio::test]
1207 async fn entity_data_service_applies_behavior_hooks() {
1208 let mut context = UserContext::new()
1209 .with_metadata(
1210 InMemoryMetadataStore::new()
1211 .with_entity(entity())
1212 .with_entity(line_entity())
1213 .with_entity(product_entity()),
1214 )
1215 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1216 .with_entity_data_service_behavior_registry(
1217 InMemoryEntityDataServiceBehaviorRegistry::new()
1218 .with_behavior("Order", OrderBehavior),
1219 );
1220 context.insert_resource(PostgresDialect);
1221 context.insert_resource(StubExecutor {
1222 affected: 1,
1223 rows: Vec::new(),
1224 });
1225
1226 let repo = context
1227 .entity_data_service::<StubExecutor>("Order")
1228 .unwrap();
1229
1230 let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
1234 let affected = repo.insert_internal(&insert).await.unwrap();
1235 assert_eq!(affected, 1);
1236 assert_eq!(repo.relation_loads(), vec!["lines".to_owned()]);
1237 }
1238
1239 #[tokio::test]
1240 async fn entity_data_service_applies_request_policy_after_behavior_hooks() {
1241 let mut context = UserContext::new()
1242 .with_metadata(
1243 InMemoryMetadataStore::new()
1244 .with_entity(entity())
1245 .with_entity(line_entity())
1246 .with_entity(product_entity()),
1247 )
1248 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1249 .with_entity_data_service_behavior_registry(
1250 InMemoryEntityDataServiceBehaviorRegistry::new()
1251 .with_behavior("Order", OrderBehavior),
1252 )
1253 .with_request_policy(TenantRequestPolicy);
1254 context.insert_named_resource("tenant_id", 9_u64);
1255 context.insert_resource(PostgresDialect);
1256 context.insert_resource(StubExecutor {
1257 affected: 1,
1258 rows: Vec::new(),
1259 });
1260
1261 let repo = context
1262 .entity_data_service::<StubExecutor>("Order")
1263 .unwrap();
1264
1265 let insert = repo.insert_command().value("id", 1_u64).value("name", "n");
1270 let command = repo.prepare_insert_command(&insert).unwrap();
1271 assert_eq!(command.values.get("version"), Some(&Value::I64(9)));
1272 }
1273
1274 #[tokio::test]
1275 async fn entity_data_service_prepares_insert_command_with_generated_id() {
1276 let mut context = UserContext::new()
1277 .with_metadata(
1278 InMemoryMetadataStore::new()
1279 .with_entity(entity())
1280 .with_entity(line_entity())
1281 .with_entity(product_entity()),
1282 )
1283 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1284 .with_entity_data_service_behavior_registry(
1285 InMemoryEntityDataServiceBehaviorRegistry::new()
1286 .with_behavior("Order", OrderBehavior),
1287 )
1288 .with_internal_id_generator(FixedIdGenerator(42));
1289 context.insert_resource(PostgresDialect);
1290 context.insert_resource(StubExecutor {
1291 affected: 1,
1292 rows: Vec::new(),
1293 });
1294
1295 let repo = context
1296 .entity_data_service::<StubExecutor>("Order")
1297 .unwrap();
1298
1299 let prepared = repo
1300 .prepare_insert_command(&repo.insert_command().value("id", 0_u64).value("name", "n"))
1301 .unwrap();
1302
1303 assert_eq!(prepared.values.get("id"), Some(&Value::U64(42)));
1304 assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
1305 assert_eq!(
1306 prepared.values.get("name"),
1307 Some(&Value::Text("n".to_owned()))
1308 );
1309
1310 let prepared_zero_version = repo
1311 .prepare_insert_command(
1312 &repo
1313 .insert_command()
1314 .value("id", 0_u64)
1315 .value("version", 0_i64)
1316 .value("name", "zero-version"),
1317 )
1318 .unwrap();
1319 assert_eq!(
1320 prepared_zero_version.values.get("version"),
1321 Some(&Value::I64(1))
1322 );
1323 }
1324
1325 #[tokio::test]
1326 async fn custom_user_context_can_drive_insert_preparation() {
1327 let mut context = UserContext::new()
1328 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1329 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1330 .with_entity_data_service_behavior_registry(
1331 InMemoryEntityDataServiceBehaviorRegistry::new()
1332 .with_behavior("Order", ContextAwareOrderBehavior),
1333 )
1334 .with_internal_id_generator(FixedIdGenerator(99));
1335 context.insert_named_resource("tenant", String::from("acme"));
1336 context.insert_named_resource("initial_version", 7_i64);
1337 context.put_local("trace_id", "req-9");
1338 context.insert_resource(PostgresDialect);
1339 context.insert_resource(StubExecutor {
1340 affected: 1,
1341 rows: Vec::new(),
1342 });
1343
1344 let repo = context
1345 .entity_data_service::<StubExecutor>("Order")
1346 .unwrap();
1347 let prepared = repo.prepare_insert_command(&repo.insert_command()).unwrap();
1348
1349 assert_eq!(prepared.values.get("id"), Some(&Value::U64(99)));
1350 assert_eq!(prepared.values.get("version"), Some(&Value::I64(7)));
1351 assert_eq!(
1352 prepared.values.get("name"),
1353 Some(&Value::Text("acme:req-9".to_owned()))
1354 );
1355 }
1356
1357 #[tokio::test]
1358 async fn checker_registry_validates_and_fixes_insert_commands() {
1359 let mut context = UserContext::new()
1360 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1361 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1362 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1363 .with_internal_id_generator(FixedIdGenerator(77));
1364 context.insert_resource(PostgresDialect);
1365 context.insert_resource(StubExecutor {
1366 affected: 1,
1367 rows: Vec::new(),
1368 });
1369
1370 let repo = context
1371 .entity_data_service::<StubExecutor>("Order")
1372 .unwrap();
1373 let prepared = repo
1374 .prepare_insert_command(&repo.insert_command().value("name", "valid"))
1375 .unwrap();
1376
1377 assert_eq!(prepared.values.get("id"), Some(&Value::U64(77)));
1378 assert_eq!(prepared.values.get("version"), Some(&Value::I64(1)));
1379 assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1380
1381 let error = repo
1382 .prepare_insert_command(&repo.insert_command().value("name", "no"))
1383 .unwrap_err();
1384 match error {
1385 RuntimeError::Check(results) => {
1386 assert_eq!(results.len(), 1);
1387 assert_eq!(results[0].location.to_string(), "name");
1388 }
1389 other => panic!("unexpected checker error: {other:?}"),
1390 }
1391 }
1392
1393 #[test]
1394 fn metadata_not_null_constraints_are_checked_without_a_custom_checker() {
1395 let context = UserContext::new().with_metadata(
1396 InMemoryMetadataStore::new().with_entity(
1397 EntityDescriptor::new("School")
1398 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1399 .property(PropertyDescriptor::new("contact_phone", DataType::Text).not_null()),
1400 ),
1401 );
1402 let mut values = EntityValues::from(Record::from([
1403 ("id".to_owned(), Value::U64(1)),
1404 (
1405 CHECK_OBJECT_STATUS_FIELD.to_owned(),
1406 Value::from(CheckObjectStatus::Create),
1407 ),
1408 ]));
1409
1410 let error = context
1411 .check_and_fix_values("School", &mut values)
1412 .unwrap_err();
1413
1414 match error {
1415 RuntimeError::Check(results) => {
1416 assert_eq!(results.len(), 1);
1417 assert_eq!(results[0].rule, CheckRule::Required);
1418 assert_eq!(results[0].location.to_string(), "contact_phone");
1419 }
1420 other => panic!("unexpected validation error: {other:?}"),
1421 }
1422 }
1423
1424 #[test]
1425 fn metadata_not_null_constraints_allow_omitted_fields_on_partial_update() {
1426 let context = UserContext::new().with_metadata(
1427 InMemoryMetadataStore::new().with_entity(
1428 EntityDescriptor::new("School")
1429 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1430 .property(PropertyDescriptor::new("contact_phone", DataType::Text).not_null()),
1431 ),
1432 );
1433 let mut values = EntityValues::from(Record::from([
1434 ("id".to_owned(), Value::U64(1)),
1435 (
1436 CHECK_OBJECT_STATUS_FIELD.to_owned(),
1437 Value::from(CheckObjectStatus::Update),
1438 ),
1439 ]));
1440
1441 context.check_and_fix_values("School", &mut values).unwrap();
1442
1443 values.insert("contact_phone".to_owned(), Value::Null);
1444 assert!(matches!(
1445 context.check_and_fix_values("School", &mut values),
1446 Err(RuntimeError::Check(_))
1447 ));
1448 }
1449
1450 #[tokio::test]
1451 async fn typed_checker_validates_and_fixes_derived_entities_without_record_access() {
1452 let mut context = UserContext::new()
1453 .with_metadata(InMemoryMetadataStore::new().with_entity(Order::entity_descriptor()))
1454 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1455 .with_checker_registry(
1456 InMemoryCheckerRegistry::new()
1457 .with_checker(TypedEntityChecker::<Order, _>::new(TypedOrderChecker)),
1458 )
1459 .with_internal_id_generator(FixedIdGenerator(79));
1460 context.insert_resource(PostgresDialect);
1461 context.insert_resource(StubExecutor {
1462 affected: 1,
1463 rows: Vec::new(),
1464 });
1465
1466 let repo = context
1467 .entity_data_service::<StubExecutor>("Order")
1468 .unwrap();
1469 let prepared = repo
1470 .prepare_insert_command(
1471 &repo
1472 .insert_command()
1473 .value("name", "fix")
1474 .value("version", 1_i64),
1475 )
1476 .unwrap();
1477 assert_eq!(
1478 prepared.values.get("name"),
1479 Some(&Value::Text("fixed".to_owned()))
1480 );
1481 assert_eq!(prepared.values.get("id"), Some(&Value::U64(79)));
1482 assert!(!prepared.values.contains_key(CHECK_OBJECT_STATUS_FIELD));
1483
1484 let error = repo
1485 .prepare_insert_command(&repo.insert_command().value("version", 1_i64))
1486 .unwrap_err();
1487 match error {
1488 RuntimeError::Check(results) => {
1489 assert!(
1490 results
1491 .iter()
1492 .any(|result| result.rule == CheckRule::Required
1493 && result.location.to_string() == "name")
1494 );
1495 }
1496 other => panic!("unexpected typed checker error: {other:?}"),
1497 }
1498 }
1499
1500 #[test]
1501 fn typed_checker_preserves_sparse_update_boundary() {
1502 let context = UserContext::new()
1503 .with_metadata(InMemoryMetadataStore::new().with_entity(Order::entity_descriptor()))
1504 .with_checker_registry(
1505 InMemoryCheckerRegistry::new()
1506 .with_checker(TypedEntityChecker::<Order, _>::new(TypedOrderChecker)),
1507 );
1508 let mut values = EntityValues::from(Record::from([
1509 ("id".to_owned(), Value::U64(7)),
1510 ("name".to_owned(), Value::Text("valid".to_owned())),
1511 (
1512 CHECK_OBJECT_STATUS_FIELD.to_owned(),
1513 Value::from(CheckObjectStatus::Update),
1514 ),
1515 ]));
1516
1517 context.check_and_fix_values("Order", &mut values).unwrap();
1518
1519 assert_eq!(values.get("id"), Some(&Value::U64(7)));
1520 assert_eq!(values.get("name"), Some(&Value::Text("valid".to_owned())));
1521 assert!(
1522 !values.contains_key("version"),
1523 "a defaulted typed-checker field became update intent"
1524 );
1525
1526 values.insert("name".to_owned(), Value::Text("fix".to_owned()));
1527 context.check_and_fix_values("Order", &mut values).unwrap();
1528 assert_eq!(values.get("name"), Some(&Value::Text("fixed".to_owned())));
1529 assert!(
1530 !values.contains_key("version"),
1531 "checker fix expanded the sparse update"
1532 );
1533 }
1534
1535 #[tokio::test]
1536 async fn checker_registry_reports_nested_create_locations_and_fixes_records() {
1537 let context = UserContext::new()
1538 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker));
1539
1540 let mut child = EntityValues::from(Record::from([
1541 (String::from("id"), Value::U64(10)),
1542 (
1543 String::from(CHECK_OBJECT_STATUS_FIELD),
1544 Value::from(CheckObjectStatus::Create),
1545 ),
1546 ]));
1547 let error = context
1548 .check_and_fix_values_at(
1549 "Order",
1550 &mut child,
1551 &ObjectLocation::hash_root("lines").element(0),
1552 )
1553 .unwrap_err();
1554
1555 assert_eq!(child.get("version"), Some(&Value::I64(1)));
1556 match error {
1557 RuntimeError::Check(results) => {
1558 assert_eq!(results.len(), 1);
1559 assert_eq!(results[0].rule, CheckRule::Required);
1560 assert_eq!(results[0].location.to_string(), "lines[0].name");
1561 }
1562 other => panic!("unexpected checker error: {other:?}"),
1563 }
1564
1565 child.insert("name".to_owned(), Value::Text("valid child".to_owned()));
1566 context
1567 .check_and_fix_values_at(
1568 "Order",
1569 &mut child,
1570 &ObjectLocation::hash_root("lines").element(0),
1571 )
1572 .unwrap();
1573 }
1574
1575 #[tokio::test]
1576 async fn built_in_language_translators_cover_fifteen_languages() {
1577 assert_eq!(Language::ALL.len(), 15);
1578 let results = [
1579 super::CheckResult::required(ObjectLocation::hash_root("name")),
1580 super::CheckResult::min(ObjectLocation::hash_root("age"), 18_i64, 12_i64),
1581 super::CheckResult::max(ObjectLocation::hash_root("age"), 65_i64, 70_i64),
1582 super::CheckResult::min_str(ObjectLocation::hash_root("name"), 2, "x"),
1583 super::CheckResult::max_str(ObjectLocation::hash_root("name"), 8, "too long name"),
1584 ];
1585 let messages = Language::ALL
1586 .iter()
1587 .flat_map(|language| {
1588 results
1589 .iter()
1590 .map(|result| translate_check_result(*language, result))
1591 })
1592 .collect::<Vec<_>>();
1593
1594 assert_eq!(messages.len(), 75);
1595 assert!(messages.iter().all(|message| !message.is_empty()));
1596 assert!(messages.iter().all(|message| !message.contains('{')));
1597 assert!(messages.iter().any(|message| message.contains("required")));
1598 assert!(messages.iter().any(|message| message.contains("å¿…å¡«")));
1599 assert!(
1600 messages
1601 .iter()
1602 .any(|message| message.contains("obligatoire"))
1603 );
1604 assert_eq!(Language::from_code("zh-CN"), Some(Language::Chinese));
1605 assert_eq!(
1606 Language::from_code("zh-TW"),
1607 Some(Language::TraditionalChinese)
1608 );
1609 }
1610
1611 #[tokio::test]
1612 async fn user_context_language_switch_translates_checker_errors() {
1613 let mut context = UserContext::new()
1614 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
1615 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1616 .with_checker_registry(InMemoryCheckerRegistry::new().with_checker(OrderChecker))
1617 .with_internal_id_generator(FixedIdGenerator(77))
1618 .with_language(Language::Chinese);
1619 context.insert_resource(PostgresDialect);
1620 context.insert_resource(StubExecutor {
1621 affected: 1,
1622 rows: Vec::new(),
1623 });
1624
1625 let repo = context
1626 .entity_data_service::<StubExecutor>("Order")
1627 .unwrap();
1628 let error = repo
1629 .prepare_insert_command(&repo.insert_command())
1630 .unwrap_err();
1631 match error {
1632 RuntimeError::Check(results) => {
1633 assert_eq!(results.len(), 1);
1634 assert!(
1635 results[0]
1636 .message
1637 .as_ref()
1638 .is_some_and(|message| message.contains("å¿…å¡«"))
1639 );
1640 }
1641 other => panic!("unexpected checker error: {other:?}"),
1642 }
1643
1644 let mut context = UserContext::new().with_language(Language::English);
1645 context.set_language_code("es").unwrap();
1646 assert_eq!(context.language(), Language::Spanish);
1647 assert!(context.set_locale_code("invalid-code").is_err());
1648 assert_eq!(context.language(), Language::Spanish);
1649
1650 let catalog = I18nCatalog::from_json(
1651 r#"{
1652 "schema":"teaql.i18n/v1",
1653 "defaultLocale":"en",
1654 "locales":{
1655 "en":{"messages":{"checker.required":"EN {location}"},"vocabulary":{}},
1656 "es":{"messages":{"checker.required":"ES {location}"},"vocabulary":{}}
1657 }
1658 }"#,
1659 )
1660 .unwrap();
1661 context.set_i18n_catalog(Arc::new(catalog));
1662 let mut results = vec![super::CheckResult::required(ObjectLocation::hash_root(
1663 "name",
1664 ))];
1665 context.translate_check_results(&mut results);
1666 assert_eq!(results[0].message.as_deref(), Some("ES Name"));
1667 }
1668
1669 #[tokio::test]
1670 async fn user_context_event_sink_receives_data_service_mutation_events() {
1671 let events = Arc::new(Mutex::new(Vec::new()));
1672 let safe_events = Arc::new(Mutex::new(Vec::new()));
1673 let mut context = UserContext::new()
1674 .with_metadata(
1675 InMemoryMetadataStore::new()
1676 .with_entity(entity().audit_mask_fields(vec!["name".to_owned()])),
1677 )
1678 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1679 .with_internal_id_generator(FixedIdGenerator(88))
1680 .with_event_sink(RecordingEventSink {
1681 events: events.clone(),
1682 })
1683 .with_custom_event_sink(RecordingSafeEventSink {
1684 events: safe_events.clone(),
1685 });
1686 context.insert_resource(PostgresDialect);
1687 context.insert_resource(StubExecutor {
1688 affected: 1,
1689 rows: vec![Record::from([
1690 ("id".to_owned(), Value::U64(88)),
1691 ("version".to_owned(), Value::I64(1)),
1692 ("name".to_owned(), Value::Text("old".to_owned())),
1693 ])],
1694 });
1695
1696 let repo = context
1697 .entity_data_service::<StubExecutor>("Order")
1698 .unwrap();
1699 repo.insert_internal(&repo.insert_command().value("name", "created"))
1700 .await
1701 .unwrap();
1702 repo.update_internal(
1703 &repo
1704 .update_command(88_u64)
1705 .expected_version(1)
1706 .value("name", "updated"),
1707 )
1708 .await
1709 .unwrap();
1710 repo.delete_internal(&repo.delete_command(88_u64).expected_version(2))
1711 .await
1712 .unwrap();
1713 repo.recover_internal(&repo.recover_command(88_u64, -3))
1714 .await
1715 .unwrap();
1716
1717 let events = events.lock().unwrap();
1718 assert_eq!(events.len(), 4);
1719 assert_eq!(events[0].kind, RawAuditEventKind::Created);
1720 assert_eq!(events[0].entity, "Order");
1721 assert_eq!(events[0].values.get("id"), Some(&Value::U64(88)));
1722 assert_eq!(events[1].kind, RawAuditEventKind::Updated);
1723 assert_eq!(events[1].values.get("id"), Some(&Value::U64(88)));
1724 assert_eq!(events[1].values.get("version"), Some(&Value::I64(2)));
1725 assert_eq!(events[1].updated_fields, vec!["name".to_owned()]);
1726 assert_eq!(
1727 events[1]
1728 .old_values
1729 .as_ref()
1730 .and_then(|values| values.get("name")),
1731 None );
1733 assert_eq!(
1734 events[1]
1735 .new_values
1736 .as_ref()
1737 .and_then(|values| values.get("name")),
1738 Some(&Value::Text("updated".to_owned()))
1739 );
1740 assert_eq!(events[1].changes.len(), 1);
1741 assert_eq!(events[1].changes[0].field, "name");
1742 assert_eq!(
1743 events[1].changes[0].old_value,
1744 None );
1746 assert_eq!(
1747 events[1].changes[0].new_value,
1748 Some(Value::Text("updated".to_owned()))
1749 );
1750 assert_eq!(events[2].kind, RawAuditEventKind::Deleted);
1751 assert!(events[2].old_values.is_none()); assert!(events[2].new_values.is_none());
1753 assert_eq!(events[3].kind, RawAuditEventKind::Recovered);
1754 assert_eq!(
1755 events[3]
1756 .old_values
1757 .as_ref()
1758 .and_then(|values| values.get("version")),
1759 None );
1761 assert_eq!(
1762 events[3]
1763 .new_values
1764 .as_ref()
1765 .and_then(|values| values.get("version")),
1766 Some(&Value::I64(4))
1767 );
1768 assert_eq!(events[3].changes[0].field, "version");
1769 drop(events);
1770
1771 let safe_events = safe_events.lock().unwrap();
1772 assert_eq!(safe_events.len(), 4);
1773 assert_eq!(safe_events[0].kind, RawAuditEventKind::Created);
1774 let name = safe_events[0]
1775 .fields
1776 .iter()
1777 .find(|field| field.name == "name")
1778 .expect("application audit event should contain the changed name field");
1779 assert!(name.masked);
1780 assert_ne!(name.value.as_deref(), Some("created"));
1781 }
1782
1783 #[tokio::test]
1784 async fn entity_data_service_builds_relation_plans() {
1785 let mut context = UserContext::new()
1786 .with_metadata(
1787 InMemoryMetadataStore::new()
1788 .with_entity(entity())
1789 .with_entity(line_entity())
1790 .with_entity(product_entity()),
1791 )
1792 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1793 .with_entity_data_service_behavior_registry(
1794 InMemoryEntityDataServiceBehaviorRegistry::new()
1795 .with_behavior("Order", OrderBehavior),
1796 );
1797 context.insert_resource(PostgresDialect);
1798 context.insert_resource(StubExecutor {
1799 affected: 1,
1800 rows: Vec::new(),
1801 });
1802
1803 let repo = context
1804 .entity_data_service::<StubExecutor>("Order")
1805 .unwrap();
1806 let plans = repo.relation_plans().unwrap();
1807
1808 assert_eq!(plans.len(), 1);
1809 assert_eq!(plans[0].relation_name, "lines");
1810 assert_eq!(plans[0].target_entity, "OrderLine");
1811 assert_eq!(plans[0].local_key, "id");
1812 assert_eq!(plans[0].foreign_key, "order_id");
1813 assert!(plans[0].many);
1814 }
1815
1816 #[tokio::test]
1817 async fn entity_data_service_builds_relation_query_from_parent_rows() {
1818 let mut context = UserContext::new()
1819 .with_metadata(
1820 InMemoryMetadataStore::new()
1821 .with_entity(entity())
1822 .with_entity(line_entity())
1823 .with_entity(product_entity()),
1824 )
1825 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1826 .with_entity_data_service_behavior_registry(
1827 InMemoryEntityDataServiceBehaviorRegistry::new()
1828 .with_behavior("Order", OrderBehavior),
1829 );
1830 context.insert_resource(PostgresDialect);
1831 context.insert_resource(StubExecutor {
1832 affected: 1,
1833 rows: Vec::new(),
1834 });
1835
1836 let repo = context
1837 .entity_data_service::<StubExecutor>("Order")
1838 .unwrap();
1839 let parent_rows = vec![
1840 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(11))])),
1841 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(12))])),
1842 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(11))])),
1843 ];
1844
1845 let query = repo.relation_query("lines", &parent_rows).unwrap();
1846 let Some(Expr::Binary { right, .. }) = query.filter else {
1847 panic!("relation query should contain an IN filter")
1848 };
1849 let Expr::Value(Value::List(ids)) = *right else {
1850 panic!("relation IN filter should contain identity values")
1851 };
1852 assert_eq!(ids, vec![Value::U64(11), Value::U64(12)]);
1853 }
1858
1859 #[tokio::test]
1860 async fn entity_data_service_enhances_parent_rows_with_relations() {
1861 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
1862 let mut context = UserContext::new()
1863 .with_metadata(
1864 InMemoryMetadataStore::new()
1865 .with_entity(entity())
1866 .with_entity(line_entity())
1867 .with_entity(product_entity()),
1868 )
1869 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
1870 .with_entity_data_service_behavior_registry(
1871 InMemoryEntityDataServiceBehaviorRegistry::new()
1872 .with_behavior("Order", OrderBehavior),
1873 )
1874 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
1875 telemetry_events.clone(),
1876 )));
1877 context.insert_resource(PostgresDialect);
1878 context.insert_resource(StubExecutor {
1879 affected: 1,
1880 rows: vec![
1881 Record::from([
1882 (String::from("id"), Value::U64(101)),
1883 (String::from("order_id"), Value::U64(11)),
1884 (String::from("name"), Value::Text(String::from("l1"))),
1885 ]),
1886 Record::from([
1887 (String::from("id"), Value::U64(102)),
1888 (String::from("order_id"), Value::U64(11)),
1889 (String::from("name"), Value::Text(String::from("l2"))),
1890 ]),
1891 Record::from([
1892 (String::from("id"), Value::U64(201)),
1893 (String::from("order_id"), Value::U64(12)),
1894 (String::from("name"), Value::Text(String::from("l3"))),
1895 ]),
1896 ],
1897 });
1898
1899 let repo = context
1900 .entity_data_service::<StubExecutor>("Order")
1901 .unwrap();
1902 let mut parents = vec![
1903 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(11))])),
1904 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(12))])),
1905 ];
1906
1907 repo.enhance_relations_internal(&mut parents).await.unwrap();
1908
1909 match parents[0].get("lines") {
1910 Some(Value::List(lines)) => assert_eq!(lines.len(), 2),
1911 other => panic!("unexpected lines payload: {other:?}"),
1912 }
1913 match parents[1].get("lines") {
1914 Some(Value::List(lines)) => assert_eq!(lines.len(), 1),
1915 other => panic!("unexpected lines payload: {other:?}"),
1916 }
1917 assert!(
1918 telemetry_events
1919 .lock()
1920 .unwrap()
1921 .iter()
1922 .any(|event| event == "start:relation_load")
1923 );
1924 }
1925
1926 #[tokio::test]
1927 async fn relation_limit_is_partitioned_per_parent_and_rank_is_internal() {
1928 let mut rows = Vec::new();
1929 for (order_id, first_line_id) in [(11_u64, 101_u64), (12_u64, 201_u64)] {
1930 for rank in 1_u64..=3 {
1931 rows.push(Record::from([
1932 (String::from("id"), Value::U64(first_line_id + rank - 1)),
1933 (String::from("order_id"), Value::U64(order_id)),
1934 (
1935 String::from(teaql_core::PARTITION_RANK_PROPERTY),
1936 Value::U64(rank),
1937 ),
1938 ]));
1939 }
1940 }
1941
1942 let mut context = UserContext::new()
1943 .with_metadata(
1944 InMemoryMetadataStore::new()
1945 .with_entity(entity())
1946 .with_entity(line_entity()),
1947 )
1948 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
1949 context.insert_resource(PostgresDialect);
1950 context.insert_resource(CapturingQueryExecutor {
1951 rows,
1952 queries: Mutex::new(Vec::new()),
1953 });
1954
1955 let repo = context
1956 .entity_data_service::<CapturingQueryExecutor>("Order")
1957 .unwrap();
1958 let mut parents = vec![
1959 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(11))])),
1960 teaql_core::CompactRow::from_map(Record::from([(String::from("id"), Value::U64(12))])),
1961 ];
1962 let query = SelectQuery::new("Order").relation_query(
1963 "lines",
1964 SelectQuery::new("OrderLine")
1965 .order_by(OrderBy::desc("id"))
1966 .limit(3),
1967 );
1968
1969 repo.enhance_query_relations_internal(&mut parents, &query)
1970 .await
1971 .unwrap();
1972
1973 let captured = &context
1974 .get_resource::<CapturingQueryExecutor>()
1975 .unwrap()
1976 .queries
1977 .lock()
1978 .unwrap()[0];
1979 assert_eq!(captured.partition_by.as_deref(), Some("order_id"));
1980 assert_eq!(captured.slice.and_then(|slice| slice.limit), Some(3));
1981 for parent in &parents {
1982 let Some(Value::List(lines)) = parent.get("lines") else {
1983 panic!("missing relation lines")
1984 };
1985 assert_eq!(lines.len(), 3);
1986 assert!(lines.iter().all(|line| match line {
1987 Value::Object(line) => !line.contains_key(teaql_core::PARTITION_RANK_PROPERTY),
1988 _ => false,
1989 }));
1990 }
1991 }
1992
1993 #[tokio::test]
1994 async fn relation_enhancement_wraps_inverse_many_relation_as_list() {
1995 let mut context = UserContext::new()
1996 .with_metadata(
1997 InMemoryMetadataStore::new()
1998 .with_entity(OrderLineWithProductEntityRow::entity_descriptor())
1999 .with_entity(ProductWithLinesEntityRow::entity_descriptor()),
2000 )
2001 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderLine"));
2002 context.insert_resource(PostgresDialect);
2003 context.insert_resource(QueueExecutor {
2004 affected: 1,
2005 rows: Mutex::new(VecDeque::from([
2006 vec![Record::from([
2007 (String::from("id"), Value::U64(11)),
2008 (String::from("order_id"), Value::U64(7)),
2009 (String::from("name"), Value::Text(String::from("line"))),
2010 (String::from("product_id"), Value::U64(101)),
2011 ])],
2012 vec![Record::from([
2013 (String::from("id"), Value::U64(101)),
2014 (String::from("name"), Value::Text(String::from("sku"))),
2015 ])],
2016 ])),
2017 queries: Mutex::new(Vec::new()),
2018 });
2019
2020 let repo = context
2021 .entity_data_service::<QueueExecutor>("OrderLine")
2022 .unwrap();
2023 let rows = repo
2024 .fetch_enhanced_entities_internal::<OrderLineWithProductEntityRow>(
2025 &SelectQuery::new("OrderLine").relation("product"),
2026 )
2027 .await
2028 .unwrap();
2029
2030 let product = rows.data[0].product.as_ref().unwrap();
2031 assert_eq!(product.lines.data.len(), 1);
2032 assert_eq!(product.lines.data[0].id, 11);
2033 }
2034
2035 #[tokio::test]
2036 async fn generated_to_one_getter_resolves_from_runtime_module_identity_graph() {
2037 let mut context = RuntimeModule::new()
2038 .entity::<FlatTripRow>()
2039 .entity::<FlatVendorRow>()
2040 .into_context();
2041 context.insert_resource(PostgresDialect);
2042 context.insert_resource(QueueExecutor {
2043 affected: 1,
2044 rows: Mutex::new(VecDeque::from([
2045 vec![Record::from([
2046 (String::from("id"), Value::U64(11)),
2047 (String::from("vendor_id"), Value::U64(101)),
2048 ])],
2049 vec![Record::from([
2050 (String::from("id"), Value::U64(101)),
2051 (String::from("name"), Value::Text(String::from("Acme"))),
2052 ])],
2053 ])),
2054 queries: Mutex::new(Vec::new()),
2055 });
2056
2057 let repo = context
2058 .entity_data_service::<QueueExecutor>("FlatTrip")
2059 .unwrap();
2060 let rows = repo
2061 .fetch_enhanced_entities_internal::<FlatTripRow>(
2062 &SelectQuery::new("FlatTrip").relation("vendor"),
2063 )
2064 .await
2065 .unwrap();
2066
2067 assert!(rows.data[0].vendor.is_none());
2068 assert_eq!(rows.data[0].vendor().unwrap().name, "Acme");
2069 }
2070
2071 #[tokio::test]
2072 async fn generated_to_many_getter_uses_adjacency_and_mutation_copies_on_write() {
2073 let mut context = RuntimeModule::new()
2074 .entity::<FlatFleetRow>()
2075 .entity::<FlatFleetTripRow>()
2076 .into_context();
2077 context.insert_resource(PostgresDialect);
2078 context.insert_resource(QueueExecutor {
2079 affected: 1,
2080 rows: Mutex::new(VecDeque::from([
2081 vec![Record::from([(String::from("id"), Value::U64(7))])],
2082 vec![
2083 Record::from([
2084 (String::from("id"), Value::U64(11)),
2085 (String::from("fleet_id"), Value::U64(7)),
2086 (String::from("name"), Value::Text(String::from("first"))),
2087 ]),
2088 Record::from([
2089 (String::from("id"), Value::U64(12)),
2090 (String::from("fleet_id"), Value::U64(7)),
2091 (String::from("name"), Value::Text(String::from("second"))),
2092 ]),
2093 ],
2094 ])),
2095 queries: Mutex::new(Vec::new()),
2096 });
2097
2098 let repo = context
2099 .entity_data_service::<QueueExecutor>("FlatFleet")
2100 .unwrap();
2101 let mut rows = repo
2102 .fetch_enhanced_entities_internal::<FlatFleetRow>(
2103 &SelectQuery::new("FlatFleet").relation("trip_list"),
2104 )
2105 .await
2106 .unwrap();
2107 let fleet = &mut rows.data[0];
2108
2109 assert!(!fleet.trip_list.is_loaded);
2110 assert_eq!(fleet.trip_list().data.len(), 2);
2111 assert_eq!(fleet.trip_list().data[1].name, "second");
2112 fleet.trip_list_mut().push(FlatFleetTripRow {
2113 id: 13,
2114 fleet_id: 7,
2115 name: "third".to_owned(),
2116 root: EntityRoot::default(),
2117 });
2118 assert!(fleet.trip_list.is_loaded);
2119 assert_eq!(fleet.trip_list().data.len(), 3);
2120 assert_eq!(
2121 fleet
2122 .root
2123 .resolve_relation_list::<FlatFleetTripRow>("FlatFleet", 7, "trip_list")
2124 .unwrap()
2125 .data
2126 .len(),
2127 2
2128 );
2129 }
2130
2131 #[tokio::test]
2132 async fn entity_data_service_fetches_smart_list_of_entities() {
2133 let mut context = UserContext::new()
2134 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2135 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2136 context.insert_resource(PostgresDialect);
2137 context.insert_resource(StubExecutor {
2138 affected: 1,
2139 rows: vec![Record::from([
2140 (String::from("id"), Value::U64(7)),
2141 (String::from("version"), Value::I64(2)),
2142 (String::from("name"), Value::Text(String::from("typed"))),
2143 ])],
2144 });
2145
2146 let repo = context
2147 .entity_data_service::<StubExecutor>("Order")
2148 .unwrap();
2149 let rows = repo
2150 .fetch_entities_internal::<OrderEntity>(&repo.select())
2151 .await
2152 .unwrap();
2153
2154 assert_eq!(rows.len(), 1);
2155 assert_eq!(
2156 rows.first(),
2157 Some(&OrderEntity {
2158 id: 7,
2159 version: 2,
2160 name: String::from("typed"),
2161 })
2162 );
2163 }
2164
2165 #[tokio::test]
2166 async fn entity_data_service_fetches_smart_list_of_derived_entities() {
2167 let mut context = UserContext::new()
2168 .with_metadata(
2169 InMemoryMetadataStore::new().with_entity(CatalogProductRow::entity_descriptor()),
2170 )
2171 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("CatalogProduct"));
2172 context.insert_resource(PostgresDialect);
2173 context.insert_resource(StubExecutor {
2174 affected: 1,
2175 rows: vec![Record::from([
2176 (String::from("id"), Value::U64(9)),
2177 (String::from("name"), Value::Text(String::from("derived"))),
2178 ])],
2179 });
2180
2181 let repo = context
2182 .entity_data_service::<StubExecutor>("CatalogProduct")
2183 .unwrap();
2184 let rows = repo
2185 .fetch_entities_internal::<CatalogProductRow>(&repo.select())
2186 .await
2187 .unwrap();
2188
2189 assert_eq!(rows.len(), 1);
2190 assert_eq!(
2191 rows.first(),
2192 Some(&CatalogProductRow {
2193 id: 9,
2194 name: String::from("derived"),
2195 })
2196 );
2197 }
2198
2199 #[tokio::test]
2200 async fn entity_data_service_collects_dynamic_properties_for_aggregate_output() {
2201 let mut context = UserContext::new()
2202 .with_metadata(
2203 InMemoryMetadataStore::new()
2204 .with_entity(OrderAggregateDynamic::entity_descriptor()),
2205 )
2206 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("OrderAggregate"));
2207 context.insert_resource(PostgresDialect);
2208 context.insert_resource(StubExecutor {
2209 affected: 1,
2210 rows: vec![Record::from([
2211 (String::from("id"), Value::U64(1)),
2212 (String::from("lineCount"), Value::I64(3)),
2213 (String::from("amount"), Value::F64(18.5)),
2214 ])],
2215 });
2216
2217 let repo = context
2218 .entity_data_service::<StubExecutor>("OrderAggregate")
2219 .unwrap();
2220 let rows = repo
2221 .fetch_entities_internal::<OrderAggregateDynamic>(&repo.select())
2222 .await
2223 .unwrap();
2224
2225 assert_eq!(rows.len(), 1);
2226 assert_eq!(rows.data[0].id, 1);
2227 assert_eq!(rows.data[0].dynamic.get("lineCount"), Some(&Value::I64(3)));
2228 assert_eq!(rows.data[0].dynamic.get("amount"), Some(&Value::F64(18.5)));
2229 assert_eq!(
2230 rows.into_vec().into_iter().next().unwrap().into_json(),
2231 serde_json::json!({
2232 "id": 1,
2233 "lineCount": 3,
2234 "amount": 18.5
2235 })
2236 );
2237 }
2238
2239 #[tokio::test]
2240 async fn entity_data_service_executes_relation_aggregates_into_dynamic_properties() {
2241 let executor = QueueExecutor {
2242 affected: 1,
2243 rows: Mutex::new(VecDeque::from([
2244 vec![
2245 Record::from([
2246 (String::from("id"), Value::U64(1)),
2247 (String::from("version"), Value::I64(1)),
2248 (String::from("name"), Value::Text(String::from("first"))),
2249 ]),
2250 Record::from([
2251 (String::from("id"), Value::U64(2)),
2252 (String::from("version"), Value::I64(1)),
2253 (String::from("name"), Value::Text(String::from("second"))),
2254 ]),
2255 ],
2256 vec![Record::from([
2257 (String::from("order_id"), Value::U64(1)),
2258 (String::from("lineCount"), Value::I64(3)),
2259 ])],
2260 ])),
2261 queries: Mutex::new(Vec::new()),
2262 };
2263 let mut context = UserContext::new()
2264 .with_metadata(
2265 InMemoryMetadataStore::new()
2266 .with_entity(entity())
2267 .with_entity(line_entity()),
2268 )
2269 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2270 context.insert_resource(PostgresDialect);
2271 context.insert_resource(executor);
2272
2273 let repo = context
2274 .entity_data_service::<QueueExecutor>("Order")
2275 .unwrap();
2276 let rows = repo
2277 .fetch_all_with_relation_aggregates_internal(
2278 &repo
2279 .select()
2280 .project("id")
2281 .project("version")
2282 .project("name"),
2283 &[RelationAggregate::new(
2284 "lines",
2285 "lineCount",
2286 SelectQuery::new("OrderLine"),
2287 true,
2288 )],
2289 )
2290 .await
2291 .unwrap();
2292
2293 assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
2294 assert_eq!(rows[1].get("lineCount"), Some(&Value::U64(0)));
2295
2296 let executor = context.get_resource::<QueueExecutor>().unwrap();
2297 let queries = executor.queries.lock().unwrap();
2298 assert_eq!(queries.len(), 2);
2299 assert_eq!(queries[1], "SELECT ... FROM OrderLine ...");
2300 }
2301
2302 #[tokio::test]
2303 async fn entity_data_service_maps_relation_aggregate_storage_key_to_property_key() {
2304 let mut line = line_entity();
2305 line.properties
2306 .iter_mut()
2307 .find(|property| property.name == "order_id")
2308 .unwrap()
2309 .column_name = "order_ref".to_owned();
2310 let executor = QueueExecutor {
2311 affected: 1,
2312 rows: Mutex::new(VecDeque::from([
2313 vec![Record::from([
2314 (String::from("id"), Value::U64(1)),
2315 (String::from("version"), Value::I64(1)),
2316 (String::from("name"), Value::Text(String::from("first"))),
2317 ])],
2318 vec![Record::from([
2319 (String::from("order_ref"), Value::I64(1)),
2320 (String::from("lineCount"), Value::I64(3)),
2321 ])],
2322 ])),
2323 queries: Mutex::new(Vec::new()),
2324 };
2325 let mut context = UserContext::new()
2326 .with_metadata(
2327 InMemoryMetadataStore::new()
2328 .with_entity(entity())
2329 .with_entity(line),
2330 )
2331 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2332 context.insert_resource(PostgresDialect);
2333 context.insert_resource(executor);
2334
2335 let repo = context
2336 .entity_data_service::<QueueExecutor>("Order")
2337 .unwrap();
2338 let rows = repo
2339 .fetch_all_with_relation_aggregates_internal(
2340 &repo
2341 .select()
2342 .project("id")
2343 .project("version")
2344 .project("name"),
2345 &[RelationAggregate::new(
2346 "lines",
2347 "lineCount",
2348 SelectQuery::new("OrderLine"),
2349 true,
2350 )],
2351 )
2352 .await
2353 .unwrap();
2354
2355 assert_eq!(rows[0].get("lineCount"), Some(&Value::I64(3)));
2356 let executor = context.get_resource::<QueueExecutor>().unwrap();
2357 assert_eq!(
2358 executor.queries.lock().unwrap()[1],
2359 "SELECT ... FROM OrderLine ..."
2360 );
2361 }
2362
2363 #[tokio::test]
2364 async fn entity_data_service_uses_aggregation_cache_when_resource_is_registered() {
2365 let telemetry_events = Arc::new(Mutex::new(Vec::new()));
2366 let executor = QueueExecutor {
2367 affected: 1,
2368 rows: Mutex::new(VecDeque::from([vec![Record::from([(
2369 String::from("count"),
2370 Value::I64(2),
2371 )])]])),
2372 queries: Mutex::new(Vec::new()),
2373 };
2374 let mut context = UserContext::new()
2375 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2376 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"))
2377 .with_runtime_telemetry(Arc::new(RecordingRuntimeTelemetry(
2378 telemetry_events.clone(),
2379 )));
2380 context.insert_resource(PostgresDialect);
2381 context.insert_resource(executor);
2382 context.insert_resource(InMemoryAggregationCache::default());
2383
2384 let repo = context
2385 .entity_data_service::<QueueExecutor>("Order")
2386 .unwrap();
2387 let query = repo
2388 .select()
2389 .count("count")
2390 .enable_aggregation_cache_for(60_000);
2391
2392 let first = repo.fetch_all_internal(&query).await.unwrap();
2393 let second = repo.fetch_all_internal(&query).await.unwrap();
2394
2395 assert_eq!(first, second);
2396 let executor = context.get_resource::<QueueExecutor>().unwrap();
2397 assert_eq!(executor.queries.lock().unwrap().len(), 1);
2398 let events = telemetry_events.lock().unwrap();
2399 assert_eq!(
2400 events
2401 .iter()
2402 .filter(|event| event.as_str() == "start:cache")
2403 .count(),
2404 2
2405 );
2406 assert_eq!(
2407 events
2408 .iter()
2409 .filter(|event| event.as_str() == "start:provider")
2410 .count(),
2411 1
2412 );
2413 }
2414
2415 #[tokio::test]
2416 async fn continuous_page_fetch_uses_id_seek_for_the_next_page() {
2417 let rows = (91_u64..=100)
2418 .rev()
2419 .map(|id| {
2420 Record::from([
2421 (String::from("id"), Value::U64(id)),
2422 (String::from("version"), Value::I64(1)),
2423 (String::from("name"), Value::Text(format!("order-{id}"))),
2424 ])
2425 })
2426 .collect();
2427 let mut context = UserContext::new()
2428 .with_user_identifier("tenant-1:user-1")
2429 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2430 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2431 context.insert_resource(PostgresDialect);
2432 context.insert_resource(CapturingQueryExecutor {
2433 rows,
2434 queries: Mutex::new(Vec::new()),
2435 });
2436 let repo = context
2437 .entity_data_service::<CapturingQueryExecutor>("Order")
2438 .unwrap();
2439
2440 let first = SelectQuery::new("Order")
2441 .order_desc("id")
2442 .page(0, 10)
2443 .optimize_for_continuous_page_fetch_with("recent-orders", 60);
2444 repo.fetch_all_internal(&first).await.unwrap();
2445 assert_eq!(
2446 context.continuous_page_plan().as_deref(),
2447 Some("OFFSET_FALLBACK:FIRST_PAGE")
2448 );
2449
2450 let second = SelectQuery::new("Order")
2451 .order_desc("id")
2452 .page(10, 10)
2453 .optimize_for_continuous_page_fetch_with("recent-orders", 60);
2454 repo.fetch_all_internal(&second).await.unwrap();
2455 assert_eq!(
2456 context.continuous_page_plan().as_deref(),
2457 Some("CURSOR_SEEK")
2458 );
2459 assert!(context.continuous_page_cursor_id().is_some());
2460
2461 let captured = context
2462 .get_resource::<CapturingQueryExecutor>()
2463 .unwrap()
2464 .queries
2465 .lock()
2466 .unwrap();
2467 assert_eq!(
2468 captured[1].slice.as_ref().map(|slice| slice.offset),
2469 Some(0)
2470 );
2471 assert!(format!("{:?}", captured[1].filter).contains("Lt"));
2472 assert!(format!("{:?}", captured[1].filter).contains("U64(91)"));
2473 }
2474
2475 #[tokio::test]
2476 async fn id_set_pagination_reuses_ordered_ids_and_returns_exact_count() {
2477 let id_rows = (1_u64..=100)
2478 .map(|id| Record::from([(String::from("id"), Value::U64(id))]))
2479 .collect::<Vec<_>>();
2480 let entity_rows = |range: std::ops::RangeInclusive<u64>| {
2481 range
2482 .map(|id| {
2483 Record::from([
2484 (String::from("id"), Value::U64(id)),
2485 (String::from("version"), Value::I64(1)),
2486 (String::from("name"), Value::Text(format!("order-{id}"))),
2487 ])
2488 })
2489 .collect::<Vec<_>>()
2490 };
2491 let mut context = UserContext::new()
2492 .with_user_identifier("id-set-test:tenant-1:user-1")
2493 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2494 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2495 context.insert_resource(PostgresDialect);
2496 context.insert_resource(IdSetQueueExecutor {
2497 rows: Mutex::new(VecDeque::from([
2498 id_rows,
2499 entity_rows(21..=30),
2500 entity_rows(51..=60),
2501 ])),
2502 queries: Mutex::new(Vec::new()),
2503 });
2504 let repo = context
2505 .entity_data_service::<IdSetQueueExecutor>("Order")
2506 .unwrap();
2507
2508 let first = repo
2509 .fetch_enhanced_entities_with_relation_aggregates_internal::<Order>(
2510 &SelectQuery::new("Order")
2511 .projects(["id", "version", "name"])
2512 .order_asc("name")
2513 .page(20, 10)
2514 .optimize_pagination_with_id_set_config("orders", 60, 1_000),
2515 &[],
2516 )
2517 .await
2518 .unwrap();
2519 assert_eq!(first.total_count, Some(100));
2520 assert_eq!(first.first().map(|entity| entity.id), Some(21));
2521 assert_eq!(context.id_set_plan().as_deref(), Some("ID_SET_BUILD"));
2522
2523 let second = repo
2524 .fetch_enhanced_entities_with_relation_aggregates_internal::<Order>(
2525 &SelectQuery::new("Order")
2526 .projects(["id", "version", "name"])
2527 .order_asc("name")
2528 .page(50, 10)
2529 .optimize_pagination_with_id_set_config("orders", 60, 1_000),
2530 &[],
2531 )
2532 .await
2533 .unwrap();
2534 assert_eq!(second.total_count, Some(100));
2535 assert_eq!(second.first().map(|entity| entity.id), Some(51));
2536 assert_eq!(context.id_set_plan().as_deref(), Some("ID_SET_HIT"));
2537
2538 let queries = &context
2539 .get_resource::<IdSetQueueExecutor>()
2540 .unwrap()
2541 .queries
2542 .lock()
2543 .unwrap();
2544 assert_eq!(
2545 queries.len(),
2546 3,
2547 "the second page must not rebuild the ID set"
2548 );
2549 assert_eq!(queries[0].projection, vec!["id"]);
2550 assert_eq!(
2551 queries[0].slice.as_ref().and_then(|slice| slice.limit),
2552 Some(1_001)
2553 );
2554 assert_eq!(
2555 queries[0].order_by.last().map(|order| order.field.as_str()),
2556 Some("id")
2557 );
2558 assert_eq!(queries[1].slice.as_ref().map(|slice| slice.offset), Some(0));
2559 assert!(format!("{:?}", queries[1].filter).contains("U64(21)"));
2560 assert!(format!("{:?}", queries[2].filter).contains("U64(51)"));
2561 }
2562
2563 #[tokio::test]
2564 async fn id_set_pagination_limit_overflow_falls_back_without_false_count() {
2565 let mut context = UserContext::new()
2566 .with_user_identifier("id-set-overflow-test")
2567 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2568 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2569 context.insert_resource(PostgresDialect);
2570 context.insert_resource(IdSetQueueExecutor {
2571 rows: Mutex::new(VecDeque::from([
2572 (1_u64..=4)
2573 .map(|id| Record::from([(String::from("id"), Value::U64(id))]))
2574 .collect(),
2575 vec![Record::from([
2576 (String::from("id"), Value::U64(1)),
2577 (String::from("version"), Value::I64(1)),
2578 (String::from("name"), Value::Text("order-1".to_owned())),
2579 ])],
2580 ])),
2581 queries: Mutex::new(Vec::new()),
2582 });
2583 let repo = context
2584 .entity_data_service::<IdSetQueueExecutor>("Order")
2585 .unwrap();
2586 let rows = repo
2587 .fetch_enhanced_entities_with_relation_aggregates_internal::<Order>(
2588 &SelectQuery::new("Order")
2589 .projects(["id", "version", "name"])
2590 .order_asc("id")
2591 .page(0, 1)
2592 .optimize_pagination_with_id_set_config("overflow", 60, 3),
2593 &[],
2594 )
2595 .await
2596 .unwrap();
2597
2598 assert_eq!(rows.total_count, None);
2599 assert_eq!(
2600 context.id_set_plan().as_deref(),
2601 Some("ID_SET_FALLBACK_LIMIT_EXCEEDED")
2602 );
2603 assert_eq!(context.id_set_count(), Some(4));
2604 }
2605
2606 #[tokio::test]
2607 async fn id_set_pagination_coalesces_concurrent_cache_misses() {
2608 let executor = ConcurrentIdSetExecutor::default();
2609 let id_queries = executor.id_queries.clone();
2610 let store: Arc<dyn crate::IdSetStore> = Arc::new(crate::InMemoryIdSetStore::default());
2611 let make_context = |executor: ConcurrentIdSetExecutor| {
2612 let mut context = UserContext::new()
2613 .with_user_identifier("id-set-single-flight-user")
2614 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2615 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2616 context.set_id_set_store(store.clone());
2617 context.insert_resource(PostgresDialect);
2618 context.insert_resource(executor);
2619 context
2620 };
2621 let first_context = make_context(executor.clone());
2622 let second_context = make_context(executor);
2623 let query = SelectQuery::new("Order")
2624 .projects(["id", "version", "name"])
2625 .order_asc("id")
2626 .page(0, 1)
2627 .optimize_pagination_with_id_set_config("single-flight", 60, 100);
2628
2629 let first = async {
2630 first_context
2631 .entity_data_service::<ConcurrentIdSetExecutor>("Order")
2632 .unwrap()
2633 .fetch_enhanced_entities_internal::<Order>(&query)
2634 .await
2635 .unwrap()
2636 };
2637 let second = async {
2638 second_context
2639 .entity_data_service::<ConcurrentIdSetExecutor>("Order")
2640 .unwrap()
2641 .fetch_enhanced_entities_internal::<Order>(&query)
2642 .await
2643 .unwrap()
2644 };
2645 let (first, second) = tokio::join!(first, second);
2646
2647 assert_eq!(first.total_count, Some(2));
2648 assert_eq!(second.total_count, Some(2));
2649 assert_eq!(
2650 id_queries.load(std::sync::atomic::Ordering::SeqCst),
2651 1,
2652 "concurrent misses must share one ID-only build"
2653 );
2654 }
2655
2656 #[tokio::test]
2657 async fn id_set_pagination_rebuilds_after_ttl_expiry() {
2658 let executor = ConcurrentIdSetExecutor::default();
2659 let id_queries = executor.id_queries.clone();
2660 let mut context = UserContext::new()
2661 .with_user_identifier("id-set-ttl-user")
2662 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2663 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2664 context.set_id_set_store(Arc::new(crate::InMemoryIdSetStore::default()));
2665 context.insert_resource(PostgresDialect);
2666 context.insert_resource(executor);
2667 let query = SelectQuery::new("Order")
2668 .projects(["id", "version", "name"])
2669 .order_asc("id")
2670 .page(0, 1)
2671 .optimize_pagination_with_id_set_config("ttl", 1, 100);
2672 let repo = context
2673 .entity_data_service::<ConcurrentIdSetExecutor>("Order")
2674 .unwrap();
2675
2676 repo.fetch_enhanced_entities_internal::<Order>(&query)
2677 .await
2678 .unwrap();
2679 tokio::time::sleep(std::time::Duration::from_millis(1_050)).await;
2680 repo.fetch_enhanced_entities_internal::<Order>(&query)
2681 .await
2682 .unwrap();
2683
2684 assert_eq!(id_queries.load(std::sync::atomic::Ordering::SeqCst), 2);
2685 assert_eq!(context.id_set_plan().as_deref(), Some("ID_SET_BUILD"));
2686 }
2687
2688 #[tokio::test]
2689 async fn id_set_pagination_isolates_principals_in_a_shared_store() {
2690 let executor = ConcurrentIdSetExecutor::default();
2691 let id_queries = executor.id_queries.clone();
2692 let store: Arc<dyn crate::IdSetStore> = Arc::new(crate::InMemoryIdSetStore::default());
2693 let make_context = |user: &str| {
2694 let mut context = UserContext::new()
2695 .with_user_identifier(user)
2696 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2697 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2698 context.set_id_set_store(store.clone());
2699 context.insert_resource(PostgresDialect);
2700 context.insert_resource(executor.clone());
2701 context
2702 };
2703 let first_context = make_context("tenant-1:user-1");
2704 let second_context = make_context("tenant-1:user-2");
2705 let query = SelectQuery::new("Order")
2706 .projects(["id", "version", "name"])
2707 .order_asc("id")
2708 .page(0, 1)
2709 .optimize_pagination_with_id_set_config("principal-isolation", 60, 100);
2710
2711 first_context
2712 .entity_data_service::<ConcurrentIdSetExecutor>("Order")
2713 .unwrap()
2714 .fetch_enhanced_entities_internal::<Order>(&query)
2715 .await
2716 .unwrap();
2717 second_context
2718 .entity_data_service::<ConcurrentIdSetExecutor>("Order")
2719 .unwrap()
2720 .fetch_enhanced_entities_internal::<Order>(&query)
2721 .await
2722 .unwrap();
2723
2724 assert_eq!(
2725 id_queries.load(std::sync::atomic::Ordering::SeqCst),
2726 2,
2727 "different principals must not share retained IDs"
2728 );
2729 }
2730
2731 #[tokio::test]
2732 async fn id_set_pagination_retains_empty_exact_result() {
2733 let mut context = UserContext::new()
2734 .with_user_identifier("id-set-empty-user")
2735 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2736 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2737 context.insert_resource(PostgresDialect);
2738 context.insert_resource(IdSetQueueExecutor {
2739 rows: Mutex::new(VecDeque::from([Vec::new(), Vec::new()])),
2740 queries: Mutex::new(Vec::new()),
2741 });
2742 let query = SelectQuery::new("Order")
2743 .projects(["id", "version", "name"])
2744 .order_asc("id")
2745 .page(0, 10)
2746 .optimize_pagination_with_id_set_config("empty", 60, 100);
2747
2748 let rows = context
2749 .entity_data_service::<IdSetQueueExecutor>("Order")
2750 .unwrap()
2751 .fetch_enhanced_entities_internal::<Order>(&query)
2752 .await
2753 .unwrap();
2754
2755 assert!(rows.is_empty());
2756 assert_eq!(rows.total_count, Some(0));
2757 assert_eq!(context.id_set_plan().as_deref(), Some("ID_SET_BUILD"));
2758 }
2759
2760 #[tokio::test]
2761 async fn id_set_pagination_store_failure_falls_back_without_changing_rows() {
2762 let mut context = UserContext::new()
2763 .with_user_identifier("id-set-store-failure-user")
2764 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2765 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2766 context.set_id_set_store(Arc::new(UnavailableIdSetStore));
2767 context.insert_resource(PostgresDialect);
2768 context.insert_resource(IdSetQueueExecutor {
2769 rows: Mutex::new(VecDeque::from([vec![Record::from([
2770 (String::from("id"), Value::U64(7)),
2771 (String::from("version"), Value::I64(1)),
2772 (String::from("name"), Value::Text("order-7".to_owned())),
2773 ])]])),
2774 queries: Mutex::new(Vec::new()),
2775 });
2776 let query = SelectQuery::new("Order")
2777 .projects(["id", "version", "name"])
2778 .order_asc("id")
2779 .page(0, 10)
2780 .optimize_pagination_with_id_set_config("unavailable", 60, 100);
2781
2782 let rows = context
2783 .entity_data_service::<IdSetQueueExecutor>("Order")
2784 .unwrap()
2785 .fetch_enhanced_entities_internal::<Order>(&query)
2786 .await
2787 .unwrap();
2788
2789 assert_eq!(rows.first().map(|row| row.id), Some(7));
2790 assert_eq!(rows.total_count, None);
2791 assert_eq!(
2792 context.id_set_plan().as_deref(),
2793 Some("ID_SET_FALLBACK_STORE_UNAVAILABLE")
2794 );
2795 }
2796
2797 #[tokio::test]
2798 async fn id_set_pagination_does_not_shift_page_when_an_entity_disappears() {
2799 let mut context = UserContext::new()
2800 .with_user_identifier("id-set-delete-user")
2801 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2802 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2803 context.insert_resource(PostgresDialect);
2804 context.insert_resource(IdSetQueueExecutor {
2805 rows: Mutex::new(VecDeque::from([
2806 vec![
2807 Record::from([(String::from("id"), Value::U64(1))]),
2808 Record::from([(String::from("id"), Value::U64(2))]),
2809 ],
2810 vec![Record::from([
2811 (String::from("id"), Value::U64(2)),
2812 (String::from("version"), Value::I64(1)),
2813 (String::from("name"), Value::Text("order-2".to_owned())),
2814 ])],
2815 ])),
2816 queries: Mutex::new(Vec::new()),
2817 });
2818 let query = SelectQuery::new("Order")
2819 .projects(["id", "version", "name"])
2820 .order_asc("id")
2821 .page(0, 2)
2822 .optimize_pagination_with_id_set_config("delete", 60, 100);
2823
2824 let rows = context
2825 .entity_data_service::<IdSetQueueExecutor>("Order")
2826 .unwrap()
2827 .fetch_enhanced_entities_internal::<Order>(&query)
2828 .await
2829 .unwrap();
2830
2831 assert_eq!(rows.total_count, Some(2));
2832 assert_eq!(rows.len(), 1);
2833 assert_eq!(rows.first().map(|row| row.id), Some(2));
2834 }
2835
2836 #[tokio::test]
2837 async fn id_set_pagination_unsupported_shape_falls_back_visibly() {
2838 let mut context = UserContext::new()
2839 .with_user_identifier("id-set-unsupported-user")
2840 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2841 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2842 context.insert_resource(PostgresDialect);
2843 context.insert_resource(IdSetQueueExecutor {
2844 rows: Mutex::new(VecDeque::from([vec![Record::from([
2845 (String::from("id"), Value::U64(9)),
2846 (String::from("version"), Value::I64(1)),
2847 (String::from("name"), Value::Text("order-9".to_owned())),
2848 ])]])),
2849 queries: Mutex::new(Vec::new()),
2850 });
2851 let query = SelectQuery::new("Order")
2852 .projects(["id", "version", "name"])
2853 .order_expr_asc(Expr::column("name"))
2854 .page(0, 10)
2855 .optimize_pagination_with_id_set_config("unsupported", 60, 100);
2856
2857 let rows = context
2858 .entity_data_service::<IdSetQueueExecutor>("Order")
2859 .unwrap()
2860 .fetch_enhanced_entities_internal::<Order>(&query)
2861 .await
2862 .unwrap();
2863
2864 assert_eq!(rows.first().map(|row| row.id), Some(9));
2865 assert_eq!(
2866 context.id_set_plan().as_deref(),
2867 Some("ID_SET_FALLBACK_UNSUPPORTED_SHAPE")
2868 );
2869 }
2870
2871 #[tokio::test]
2872 async fn aggregation_cache_is_namespaced_and_invalidated_after_write() {
2873 let executor = QueueExecutor {
2874 affected: 1,
2875 rows: Mutex::new(VecDeque::from([
2876 vec![Record::from([(String::from("count"), Value::I64(2))])],
2877 vec![Record::from([(String::from("count"), Value::I64(3))])],
2878 ])),
2879 queries: Mutex::new(Vec::new()),
2880 };
2881 let mut context = UserContext::new()
2882 .with_metadata(InMemoryMetadataStore::new().with_entity(entity()))
2883 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2884 context.insert_resource(PostgresDialect);
2885 context.insert_resource(executor);
2886 context.insert_resource(
2887 Arc::new(InMemoryAggregationCache::with_namespace("tenant-a"))
2888 as Arc<dyn AggregationCacheBackend>,
2889 );
2890
2891 let repo = context
2892 .entity_data_service::<QueueExecutor>("Order")
2893 .unwrap();
2894 let query = repo
2895 .select()
2896 .count("count")
2897 .enable_aggregation_cache_for(60_000);
2898
2899 let first = repo.fetch_all_internal(&query).await.unwrap();
2900 let cached = repo.fetch_all_internal(&query).await.unwrap();
2901 repo.insert_internal(
2902 &InsertCommand::new("Order")
2903 .value("id", 9_u64)
2904 .value("version", 1_i64)
2905 .value("name", "new"),
2906 )
2907 .await
2908 .unwrap();
2909 let refreshed = repo.fetch_all_internal(&query).await.unwrap();
2910
2911 assert_eq!(first, cached);
2912 assert_ne!(cached, refreshed);
2913 let executor = context.get_resource::<QueueExecutor>().unwrap();
2914 assert_eq!(executor.queries.lock().unwrap().len(), 2);
2915 }
2916
2917 #[tokio::test]
2918 async fn aggregation_cache_propagates_to_relation_aggregates() {
2919 let parent_rows = vec![
2920 Record::from([
2921 (String::from("id"), Value::U64(1)),
2922 (String::from("version"), Value::I64(1)),
2923 (String::from("name"), Value::Text(String::from("first"))),
2924 ]),
2925 Record::from([
2926 (String::from("id"), Value::U64(2)),
2927 (String::from("version"), Value::I64(1)),
2928 (String::from("name"), Value::Text(String::from("second"))),
2929 ]),
2930 ];
2931 let aggregate_rows = vec![Record::from([
2932 (String::from("order_id"), Value::U64(1)),
2933 (String::from("lineCount"), Value::I64(3)),
2934 ])];
2935 let executor = QueueExecutor {
2936 affected: 1,
2937 rows: Mutex::new(VecDeque::from([parent_rows, aggregate_rows])),
2938 queries: Mutex::new(Vec::new()),
2939 };
2940 let mut context = UserContext::new()
2941 .with_metadata(
2942 InMemoryMetadataStore::new()
2943 .with_entity(entity())
2944 .with_entity(line_entity()),
2945 )
2946 .with_entity_registry(InMemoryEntityRegistry::new().with_entity("Order"));
2947 context.insert_resource(PostgresDialect);
2948 context.insert_resource(executor);
2949 context.insert_resource(InMemoryAggregationCache::default());
2950
2951 let repo = context
2952 .entity_data_service::<QueueExecutor>("Order")
2953 .unwrap();
2954 let query = repo
2955 .select()
2956 .project("id")
2957 .project("version")
2958 .project("name")
2959 .enable_aggregation_cache_for(60_000)
2960 .propagate_aggregation_cache(60_000);
2961 let aggregate =
2962 RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
2963
2964 let first = repo
2965 .fetch_all_with_relation_aggregates_internal(&query, &[aggregate.clone()])
2966 .await
2967 .unwrap();
2968 let second = repo
2969 .fetch_all_with_relation_aggregates_internal(&query, &[aggregate])
2970 .await
2971 .unwrap();
2972
2973 let executor = context.get_resource::<QueueExecutor>().unwrap();
2974 assert_eq!(executor.queries.lock().unwrap().len(), 2);
2975 assert_eq!(first, second);
2976 }
2977
2978 #[tokio::test]
2979 async fn memory_data_service_fetches_smart_list_entities_with_query_features() {
2980 let metadata = InMemoryMetadataStore::new().with_entity(entity());
2981 let data_service = MemoryDataService::new(metadata).with_rows(
2982 "Order",
2983 vec![
2984 Record::from([
2985 (String::from("id"), Value::U64(1)),
2986 (String::from("version"), Value::I64(1)),
2987 (String::from("name"), Value::Text(String::from("alpha"))),
2988 ]),
2989 Record::from([
2990 (String::from("id"), Value::U64(2)),
2991 (String::from("version"), Value::I64(1)),
2992 (String::from("name"), Value::Text(String::from("beta"))),
2993 ]),
2994 Record::from([
2995 (String::from("id"), Value::U64(3)),
2996 (String::from("version"), Value::I64(1)),
2997 (String::from("name"), Value::Text(String::from("gamma"))),
2998 ]),
2999 ],
3000 );
3001
3002 let query = teaql_core::SelectQuery::new("Order")
3003 .filter(Expr::Binary {
3004 left: Box::new(Expr::column("id")),
3005 op: teaql_core::BinaryOp::Gte,
3006 right: Box::new(Expr::value(2_u64)),
3007 })
3008 .order_by(OrderBy::desc("id"))
3009 .limit(1);
3010
3011 let orders = data_service.fetch_entities::<Order>(&query).unwrap();
3012
3013 assert_eq!(orders.ids(), vec![Value::U64(3)]);
3014 assert_eq!(orders.versions(), vec![1]);
3015 assert_eq!(orders.first().unwrap().name, "gamma");
3016 }
3017
3018 #[tokio::test]
3019 async fn memory_data_service_runs_relation_aggregates() {
3020 let metadata = InMemoryMetadataStore::new()
3021 .with_entity(entity())
3022 .with_entity(line_entity());
3023
3024 let data_service = MemoryDataService::new(metadata)
3025 .with_rows(
3026 "Order",
3027 vec![
3028 Record::from([
3029 (String::from("id"), Value::U64(1)),
3030 (String::from("version"), Value::I64(1)),
3031 (String::from("name"), Value::Text(String::from("first"))),
3032 ]),
3033 Record::from([
3034 (String::from("id"), Value::U64(2)),
3035 (String::from("version"), Value::I64(1)),
3036 (String::from("name"), Value::Text(String::from("second"))),
3037 ]),
3038 ],
3039 )
3040 .with_rows(
3041 "OrderLine",
3042 vec![
3043 Record::from([
3044 (String::from("id"), Value::U64(10)),
3045 (String::from("version"), Value::I64(1)),
3046 (String::from("order_id"), Value::U64(1)),
3047 (String::from("name"), Value::Text(String::from("line1"))),
3048 ]),
3049 Record::from([
3050 (String::from("id"), Value::U64(11)),
3051 (String::from("version"), Value::I64(1)),
3052 (String::from("order_id"), Value::U64(1)),
3053 (String::from("name"), Value::Text(String::from("line2"))),
3054 ]),
3055 Record::from([
3056 (String::from("id"), Value::U64(12)),
3057 (String::from("version"), Value::I64(1)),
3058 (String::from("order_id"), Value::U64(2)),
3059 (String::from("name"), Value::Text(String::from("line3"))),
3060 ]),
3061 ],
3062 );
3063
3064 let query = SelectQuery::new("Order").project("id").project("name");
3065 let aggregate =
3066 RelationAggregate::new("lines", "lineCount", SelectQuery::new("OrderLine"), true);
3067
3068 let rows = data_service
3069 .fetch_all_with_relation_aggregates(&query, &[aggregate])
3070 .unwrap();
3071
3072 assert_eq!(rows.len(), 2);
3073
3074 let first_order = rows
3075 .iter()
3076 .find(|r| r.get("id") == Some(&Value::U64(1)))
3077 .unwrap();
3078 assert_eq!(first_order.get("lineCount"), Some(&Value::U64(2)));
3079
3080 let second_order = rows
3081 .iter()
3082 .find(|r| r.get("id") == Some(&Value::U64(2)))
3083 .unwrap();
3084 assert_eq!(second_order.get("lineCount"), Some(&Value::U64(1)));
3085 }
3086
3087 #[tokio::test]
3088 async fn memory_data_service_runs_aggregates() {
3089 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3090 let data_service = MemoryDataService::new(metadata).with_rows(
3091 "Order",
3092 vec![
3093 Record::from([
3094 (String::from("id"), Value::U64(1)),
3095 (String::from("version"), Value::I64(1)),
3096 (String::from("name"), Value::Text(String::from("alpha"))),
3097 ]),
3098 Record::from([
3099 (String::from("id"), Value::U64(2)),
3100 (String::from("version"), Value::I64(2)),
3101 (String::from("name"), Value::Text(String::from("beta"))),
3102 ]),
3103 ],
3104 );
3105
3106 let query = teaql_core::SelectQuery {
3107 hard_limit: 10_000,
3108 entity: String::from("Order"),
3109 projection: Vec::new(),
3110 expr_projection: Vec::new(),
3111 filter: None,
3112 having: None,
3113 order_by: Vec::new(),
3114 slice: None,
3115 partition_by: None,
3116 trace_chain: Vec::new(),
3117 aggregates: vec![
3118 Aggregate {
3119 function: AggregateFunction::Count,
3120 field: String::from("id"),
3121 alias: String::from("count"),
3122 },
3123 Aggregate {
3124 function: AggregateFunction::Sum,
3125 field: String::from("version"),
3126 alias: String::from("versionSum"),
3127 },
3128 ],
3129 group_by: Vec::new(),
3130 relations: Vec::new(),
3131 aggregation_cache: None,
3132 comment: None,
3133 raw_sql: None,
3134 raw_sql_search_criteria: Vec::new(),
3135 dynamic_properties: Vec::new(),
3136 raw_projections: Vec::new(),
3137 object_group_bys: Vec::new(),
3138 search_with_text: None,
3139 child_enhancements: Vec::new(),
3140 stream_config: None,
3141 continuous_page_fetch: None,
3142 id_set_pagination: None,
3143 };
3144
3145 let rows = data_service.fetch_all(&query).unwrap();
3146
3147 assert_eq!(rows.len(), 1);
3148 assert_eq!(rows[0].get("count"), Some(&Value::U64(2)));
3149 assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
3150 }
3151
3152 #[tokio::test]
3153 async fn memory_data_service_runs_grouped_aggregates_and_extended_filters() {
3154 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3155 let data_service = MemoryDataService::new(metadata).with_rows(
3156 "Order",
3157 vec![
3158 Record::from([
3159 (String::from("id"), Value::U64(1)),
3160 (String::from("version"), Value::I64(1)),
3161 (String::from("name"), Value::Text(String::from("alpha"))),
3162 ]),
3163 Record::from([
3164 (String::from("id"), Value::U64(2)),
3165 (String::from("version"), Value::I64(2)),
3166 (String::from("name"), Value::Text(String::from("alpha"))),
3167 ]),
3168 Record::from([
3169 (String::from("id"), Value::U64(3)),
3170 (String::from("version"), Value::I64(3)),
3171 (String::from("name"), Value::Text(String::from("tmp-beta"))),
3172 ]),
3173 ],
3174 );
3175
3176 let rows = data_service
3177 .fetch_all(
3178 &teaql_core::SelectQuery::new("Order")
3179 .filter(
3180 Expr::between("version", 1_i64, 3_i64)
3181 .and_expr(Expr::not_like("name", "tmp%"))
3182 .and_expr(Expr::not_in_list("name", vec![Value::from("deleted")])),
3183 )
3184 .group_by("name")
3185 .count("total")
3186 .sum("version", "versionSum"),
3187 )
3188 .unwrap();
3189
3190 assert_eq!(rows.len(), 1);
3191 assert_eq!(
3192 rows[0].get("name"),
3193 Some(&Value::Text(String::from("alpha")))
3194 );
3195 assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
3196 assert_eq!(rows[0].get("versionSum"), Some(&Value::U64(3)));
3197 }
3198
3199 #[tokio::test]
3200 async fn memory_data_service_runs_extended_aggregates_and_having() {
3201 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3202 let data_service = MemoryDataService::new(metadata).with_rows(
3203 "Order",
3204 vec![
3205 Record::from([
3206 (String::from("id"), Value::U64(1)),
3207 (String::from("version"), Value::I64(1)),
3208 (String::from("name"), Value::Text(String::from("alpha"))),
3209 ]),
3210 Record::from([
3211 (String::from("id"), Value::U64(2)),
3212 (String::from("version"), Value::I64(3)),
3213 (String::from("name"), Value::Text(String::from("alpha"))),
3214 ]),
3215 Record::from([
3216 (String::from("id"), Value::U64(3)),
3217 (String::from("version"), Value::I64(7)),
3218 (String::from("name"), Value::Text(String::from("beta"))),
3219 ]),
3220 ],
3221 );
3222
3223 let rows = data_service
3224 .fetch_all(
3225 &teaql_core::SelectQuery::new("Order")
3226 .group_by("name")
3227 .count("total")
3228 .stddev("version", "stddevVersion")
3229 .var_pop("version", "varPopVersion")
3230 .bit_or("version", "bitOrVersion")
3231 .having(Expr::gt("total", 1_i64)),
3232 )
3233 .unwrap();
3234
3235 assert_eq!(rows.len(), 1);
3236 assert_eq!(
3237 rows[0].get("name"),
3238 Some(&Value::Text(String::from("alpha")))
3239 );
3240 assert_eq!(rows[0].get("total"), Some(&Value::U64(2)));
3241 assert_eq!(
3242 rows[0].get("stddevVersion").map(Value::to_json_value),
3243 Some(serde_json::Value::String(
3244 "1.4142135623730951454746218583".to_owned()
3245 ))
3246 );
3247 assert_eq!(
3248 rows[0].get("varPopVersion"),
3249 Some(&Value::Decimal(Decimal::ONE))
3250 );
3251 assert_eq!(rows[0].get("bitOrVersion"), Some(&Value::I64(3)));
3252 }
3253
3254 #[tokio::test]
3255 async fn memory_data_service_runs_sound_like_filter() {
3256 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3257 let data_service = MemoryDataService::new(metadata).with_rows(
3258 "Order",
3259 vec![
3260 Record::from([
3261 (String::from("id"), Value::U64(1)),
3262 (String::from("version"), Value::I64(1)),
3263 (String::from("name"), Value::Text(String::from("Robert"))),
3264 ]),
3265 Record::from([
3266 (String::from("id"), Value::U64(2)),
3267 (String::from("version"), Value::I64(1)),
3268 (String::from("name"), Value::Text(String::from("Rupert"))),
3269 ]),
3270 Record::from([
3271 (String::from("id"), Value::U64(3)),
3272 (String::from("version"), Value::I64(1)),
3273 (String::from("name"), Value::Text(String::from("Ashcraft"))),
3274 ]),
3275 ],
3276 );
3277
3278 let rows = data_service
3279 .fetch_all(
3280 &teaql_core::SelectQuery::new("Order")
3281 .filter(Expr::sound_like("name", "Robert"))
3282 .order_asc("id"),
3283 )
3284 .unwrap();
3285
3286 assert_eq!(rows.len(), 2);
3287 assert_eq!(rows[0].get("name"), Some(&Value::Text("Robert".to_owned())));
3288 assert_eq!(rows[1].get("name"), Some(&Value::Text("Rupert".to_owned())));
3289 }
3290
3291 #[tokio::test]
3292 async fn memory_data_service_runs_java_style_string_match_filters() {
3293 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3294 let data_service = MemoryDataService::new(metadata).with_rows(
3295 "Order",
3296 vec![
3297 Record::from([
3298 (String::from("id"), Value::U64(1)),
3299 (String::from("version"), Value::I64(1)),
3300 (String::from("name"), Value::Text(String::from("tea-order"))),
3301 ]),
3302 Record::from([
3303 (String::from("id"), Value::U64(2)),
3304 (String::from("version"), Value::I64(1)),
3305 (
3306 String::from("name"),
3307 Value::Text(String::from("coffee-order")),
3308 ),
3309 ]),
3310 Record::from([
3311 (String::from("id"), Value::U64(3)),
3312 (String::from("version"), Value::I64(1)),
3313 (
3314 String::from("name"),
3315 Value::Text(String::from("tea-archived")),
3316 ),
3317 ]),
3318 ],
3319 );
3320
3321 let rows = data_service
3322 .fetch_all(
3323 &teaql_core::SelectQuery::new("Order")
3324 .filter(
3325 Expr::contain("name", "tea")
3326 .and_expr(Expr::begin_with("name", "tea"))
3327 .and_expr(Expr::end_with("name", "order"))
3328 .and_expr(Expr::not_contain("name", "coffee"))
3329 .and_expr(Expr::not_begin_with("name", "archived"))
3330 .and_expr(Expr::not_end_with("name", "draft")),
3331 )
3332 .order_asc("id"),
3333 )
3334 .unwrap();
3335
3336 assert_eq!(rows.len(), 1);
3337 assert_eq!(
3338 rows[0].get("name"),
3339 Some(&Value::Text("tea-order".to_owned()))
3340 );
3341 }
3342
3343 #[tokio::test]
3344 async fn memory_data_service_runs_property_to_property_filters() {
3345 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3346 let data_service = MemoryDataService::new(metadata).with_rows(
3347 "Order",
3348 vec![
3349 Record::from([
3350 (String::from("id"), Value::U64(1)),
3351 (String::from("version"), Value::I64(2)),
3352 (String::from("name"), Value::Text(String::from("keep"))),
3353 ]),
3354 Record::from([
3355 (String::from("id"), Value::U64(2)),
3356 (String::from("version"), Value::I64(1)),
3357 (String::from("name"), Value::Text(String::from("skip"))),
3358 ]),
3359 ],
3360 );
3361
3362 let rows = data_service
3363 .fetch_all(
3364 &teaql_core::SelectQuery::new("Order")
3365 .filter(Expr::compare_columns("version", BinaryOp::Gte, "id"))
3366 .order_asc("id"),
3367 )
3368 .unwrap();
3369
3370 assert_eq!(rows.len(), 1);
3371 assert_eq!(rows[0].get("name"), Some(&Value::Text("keep".to_owned())));
3372 }
3373
3374 #[tokio::test]
3375 async fn memory_data_service_supports_mutations_and_optimistic_locking() {
3376 let metadata = InMemoryMetadataStore::new().with_entity(entity());
3377 let data_service = MemoryDataService::new(metadata);
3378
3379 data_service
3380 .insert(
3381 &InsertCommand::new("Order")
3382 .value("id", 10_u64)
3383 .value("version", 1_i64)
3384 .value("name", "draft"),
3385 )
3386 .unwrap();
3387 data_service
3388 .update(
3389 &UpdateCommand::new("Order", 10_u64)
3390 .expected_version(1)
3391 .value("name", "submitted"),
3392 )
3393 .unwrap();
3394
3395 let row = data_service
3396 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
3397 .unwrap()
3398 .pop()
3399 .unwrap();
3400 assert_eq!(
3401 row.get("name"),
3402 Some(&Value::Text(String::from("submitted")))
3403 );
3404 assert_eq!(row.get("version"), Some(&Value::I64(2)));
3405
3406 let conflict = data_service
3407 .update(
3408 &UpdateCommand::new("Order", 10_u64)
3409 .expected_version(1)
3410 .value("name", "stale"),
3411 )
3412 .unwrap_err();
3413 assert!(matches!(
3414 conflict,
3415 DataServiceError::Runtime(RuntimeError::OptimisticLockConflict { .. })
3416 ));
3417
3418 data_service
3419 .delete(&DeleteCommand::new("Order", 10_u64).expected_version(2))
3420 .unwrap();
3421 let row = data_service
3422 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
3423 .unwrap()
3424 .pop()
3425 .unwrap();
3426 assert_eq!(row.get("version"), Some(&Value::I64(-3)));
3427
3428 data_service
3429 .recover(&RecoverCommand::new("Order", 10_u64, -3))
3430 .unwrap();
3431 let row = data_service
3432 .fetch_all(&teaql_core::SelectQuery::new("Order").filter(Expr::eq("id", 10_u64)))
3433 .unwrap()
3434 .pop()
3435 .unwrap();
3436 assert_eq!(row.get("version"), Some(&Value::I64(4)));
3437 }
3438
3439 #[tokio::test]
3440 async fn user_context_reports_missing_schema_provider() {
3441 let err = UserContext::new().ensure_schema().await.unwrap_err();
3442 assert!(
3443 matches!(err, RuntimeError::Schema(message) if message == "missing schema provider")
3444 );
3445 }
3446
3447 #[tokio::test]
3448 async fn user_context_stores_and_exposes_user_identifier() {
3449 let mut context = UserContext::new();
3450 let pid = std::process::id();
3451 let thread_id_str = format!("{:?}", std::thread::current().id());
3452 let numeric_thread_id = thread_id_str
3453 .strip_prefix("ThreadId(")
3454 .and_then(|s| s.strip_suffix(")"))
3455 .unwrap_or(&thread_id_str);
3456 let os_user = std::env::var("USER")
3457 .or_else(|_| std::env::var("USERNAME"))
3458 .unwrap_or_else(|_| "main".to_owned());
3459 let expected_default = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
3460 assert_eq!(context.user_identifier(), Some(expected_default.as_str()));
3461
3462 context.set_user_identifier("user-123");
3463 assert_eq!(context.user_identifier(), Some("user-123"));
3464
3465 let ctx2 = UserContext::new().with_user_identifier("user-456");
3466 assert_eq!(ctx2.user_identifier(), Some("user-456"));
3467
3468 let mut ctx3 = UserContext::new();
3469 ctx3.set_user_identifier_option(Some("user-789".to_owned()));
3470 assert_eq!(ctx3.user_identifier(), Some("user-789"));
3471 ctx3.set_user_identifier_option(None);
3472 assert_eq!(ctx3.user_identifier(), None);
3473
3474 let ctx4 = UserContext::new().with_user_identifier_option(Some("user-abc".to_owned()));
3475 assert_eq!(ctx4.user_identifier(), Some("user-abc"));
3476 }
3477
3478 #[test]
3479 fn local_lock_enforces_ownership_timeout_and_lease_expiry() {
3480 let first = UserContext::new();
3481 let second = UserContext::new();
3482 let key = format!("local-lock-{:?}", std::time::SystemTime::now());
3483
3484 assert!(first.try_local_lock(&key, 0, 50));
3485 assert!(!second.try_local_lock(&key, 0, 50));
3486 second.unlock_local(&key);
3487 assert!(!second.try_local_lock(&key, 0, 50));
3488 std::thread::sleep(std::time::Duration::from_millis(60));
3489 assert!(second.try_local_lock(&key, 0, 50));
3490 second.unlock_local(&key);
3491 assert!(first.try_local_lock(&key, 0, 50));
3492 first.unlock_local(&key);
3493 }
3494
3495 #[derive(Default)]
3496 struct TestRemoteLockProvider {
3497 owners: Mutex<std::collections::HashMap<String, String>>,
3498 }
3499
3500 #[async_trait::async_trait]
3501 impl RemoteLockProvider for TestRemoteLockProvider {
3502 async fn try_remote_lock(
3503 &self,
3504 key: &str,
3505 owner_token: &str,
3506 _timeout_millis: u64,
3507 _expire_millis: u64,
3508 ) -> bool {
3509 let mut owners = self.owners.lock().expect("remote lock state");
3510 if owners.contains_key(key) {
3511 return false;
3512 }
3513 owners.insert(key.to_owned(), owner_token.to_owned());
3514 true
3515 }
3516
3517 async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool {
3518 let mut owners = self.owners.lock().expect("remote lock state");
3519 if owners.get(key).is_some_and(|owner| owner == owner_token) {
3520 owners.remove(key);
3521 return true;
3522 }
3523 false
3524 }
3525 }
3526
3527 #[tokio::test]
3528 async fn remote_lock_delegates_and_preserves_context_ownership() {
3529 let provider: Arc<dyn RemoteLockProvider> = Arc::new(TestRemoteLockProvider::default());
3530 let mut first = UserContext::new();
3531 first.insert_resource(provider.clone());
3532 let mut second = UserContext::new();
3533 second.insert_resource(provider);
3534 let key = format!("remote-lock-{:?}", std::time::SystemTime::now());
3535
3536 assert!(first.try_remote_lock(&key, 0, 1_000).await);
3537 assert!(!second.try_remote_lock(&key, 0, 1_000).await);
3538 assert!(!second.unlock_remote(&key).await);
3539 assert!(!second.try_remote_lock(&key, 0, 1_000).await);
3540 assert!(first.unlock_remote(&key).await);
3541 assert!(second.try_remote_lock(&key, 0, 1_000).await);
3542 assert!(second.unlock_remote(&key).await);
3543
3544 assert!(UserContext::new().try_remote_lock("optional", 0, 0).await);
3545 }
3546}
3547
3548pub use checker::{
3549 CHECK_OBJECT_STATUS_FIELD, CheckObjectStatus, CheckResult, CheckResults, CheckRule, Checker,
3550 CheckerRegistry, InMemoryCheckerRegistry, LocationSegment, ObjectLocation, TypedChecker,
3551 TypedEntityChecker, clear_entity_status, mark_entity_status,
3552};