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