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