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