1use super::backend::{DriverBackend, QueryResultKind, TransactionType};
2use super::real_driver::RealTypeDBBackend;
3use crate::config::TypeDBSection;
4use crate::error::PipelineError;
5use crate::executor::QueryExecutor;
6
7pub struct TypeDBClient {
10 backend: Box<dyn DriverBackend>,
11}
12
13impl TypeDBClient {
14 #[cfg_attr(coverage_nightly, coverage(off))]
16 pub async fn connect(config: &TypeDBSection) -> Result<Self, PipelineError> {
17 let backend = RealTypeDBBackend::connect(config).await?;
18 Ok(Self {
19 backend: Box::new(backend),
20 })
21 }
22
23 #[cfg(test)]
25 pub(crate) fn with_backend(backend: Box<dyn DriverBackend>) -> Self {
26 Self { backend }
27 }
28
29 pub async fn execute(
34 &self,
35 database: &str,
36 typeql: &str,
37 tx_type: &str,
38 ) -> Result<serde_json::Value, PipelineError> {
39 let transaction_type = parse_transaction_type(tx_type)?;
40
41 let mut tx = self
42 .backend
43 .open_transaction(database, transaction_type)
44 .await?;
45
46 let answer = tx.query(typeql).await?;
47
48 let needs_commit = matches!(
49 transaction_type,
50 TransactionType::Write | TransactionType::Schema
51 );
52
53 let results = match answer {
54 QueryResultKind::Ok => {
55 if needs_commit {
56 tx.commit().await?;
57 }
58 serde_json::json!({ "ok": true })
59 }
60 QueryResultKind::Rows(rows) => {
61 if needs_commit {
62 tx.commit().await?;
63 }
64 serde_json::Value::Array(rows)
65 }
66 QueryResultKind::Documents(docs) => {
67 if needs_commit {
68 let _ = tx.commit().await;
69 }
70 serde_json::Value::Array(docs)
71 }
72 };
73
74 Ok(results)
75 }
76
77 pub fn is_connected(&self) -> bool {
79 self.backend.is_open()
80 }
81}
82
83impl QueryExecutor for TypeDBClient {
84 fn execute<'a>(
85 &'a self,
86 database: &'a str,
87 typeql: &'a str,
88 transaction_type: &'a str,
89 ) -> std::pin::Pin<
90 Box<dyn std::future::Future<Output = Result<serde_json::Value, PipelineError>> + Send + 'a>,
91 > {
92 Box::pin(async move { self.execute(database, typeql, transaction_type).await })
93 }
94
95 fn is_connected(&self) -> bool {
96 self.is_connected()
97 }
98}
99
100pub(crate) fn parse_transaction_type(tx_type: &str) -> Result<TransactionType, PipelineError> {
102 match tx_type {
103 "read" => Ok(TransactionType::Read),
104 "write" => Ok(TransactionType::Write),
105 "schema" => Ok(TransactionType::Schema),
106 other => Err(PipelineError::QueryExecution(format!(
107 "Unknown transaction type: {other}"
108 ))),
109 }
110}
111
112#[cfg(feature = "band7")]
116pub(crate) fn concept_to_json_b7(
117 concept: &type_bridge_typedb_driver_b7::concept::Concept,
118) -> serde_json::Value {
119 let mut obj = serde_json::Map::new();
120 obj.insert(
121 "category".to_string(),
122 serde_json::Value::String(concept.get_category().name().to_string()),
123 );
124 obj.insert(
125 "label".to_string(),
126 serde_json::Value::String(concept.get_label().to_string()),
127 );
128 if let Some(iid) = concept.try_get_iid() {
129 obj.insert(
130 "iid".to_string(),
131 serde_json::Value::String(iid.to_string()),
132 );
133 }
134 if let Some(value) = concept.try_get_value() {
135 obj.insert("value".to_string(), value_to_json_b7(value));
136 }
137 if let Some(value_type) = concept.try_get_value_type() {
138 obj.insert(
139 "value_type".to_string(),
140 serde_json::Value::String(value_type.name().to_string()),
141 );
142 }
143 serde_json::Value::Object(obj)
144}
145
146#[cfg(feature = "band7")]
148#[cfg_attr(coverage_nightly, coverage(off))]
149fn value_to_json_b7(value: &type_bridge_typedb_driver_b7::concept::Value) -> serde_json::Value {
150 if let Some(b) = value.get_boolean() {
151 return serde_json::Value::Bool(b);
152 }
153 if let Some(i) = value.get_integer() {
154 return serde_json::json!(i);
155 }
156 if let Some(d) = value.get_double() {
157 return serde_json::json!(d);
158 }
159 if let Some(s) = value.get_string() {
160 return serde_json::Value::String(s.to_string());
161 }
162 if let Some(date) = value.get_date() {
163 return serde_json::Value::String(date.to_string());
164 }
165 if let Some(dt) = value.get_datetime() {
166 return serde_json::Value::String(dt.to_string());
167 }
168 if let Some(dt_tz) = value.get_datetime_tz() {
169 return serde_json::Value::String(dt_tz.to_string());
170 }
171 if let Some(dec) = value.get_decimal() {
172 return serde_json::Value::String(dec.to_string());
173 }
174 if let Some(dur) = value.get_duration() {
175 return serde_json::Value::String(dur.to_string());
176 }
177 serde_json::Value::String(value.to_string())
178}
179
180#[cfg(feature = "band8")]
184pub(crate) fn concept_to_json_b8(concept: &typedb_driver::concept::Concept) -> serde_json::Value {
185 let mut obj = serde_json::Map::new();
186 obj.insert(
187 "category".to_string(),
188 serde_json::Value::String(concept.get_category().name().to_string()),
189 );
190 obj.insert(
191 "label".to_string(),
192 serde_json::Value::String(concept.get_label().to_string()),
193 );
194 if let Some(iid) = concept.try_get_iid() {
195 obj.insert(
196 "iid".to_string(),
197 serde_json::Value::String(iid.to_string()),
198 );
199 }
200 if let Some(value) = concept.try_get_value() {
201 obj.insert("value".to_string(), value_to_json_b8(value));
202 }
203 if let Some(value_type) = concept.try_get_value_type() {
204 obj.insert(
205 "value_type".to_string(),
206 serde_json::Value::String(value_type.name().to_string()),
207 );
208 }
209 serde_json::Value::Object(obj)
210}
211
212#[cfg(feature = "band8")]
214#[cfg_attr(coverage_nightly, coverage(off))]
215pub(crate) fn value_to_json_b8(value: &typedb_driver::concept::Value) -> serde_json::Value {
216 if let Some(b) = value.get_boolean() {
217 return serde_json::Value::Bool(b);
218 }
219 if let Some(i) = value.get_integer() {
220 return serde_json::json!(i);
221 }
222 if let Some(d) = value.get_double() {
223 return serde_json::json!(d);
224 }
225 if let Some(s) = value.get_string() {
226 return serde_json::Value::String(s.to_string());
227 }
228 if let Some(date) = value.get_date() {
229 return serde_json::Value::String(date.to_string());
230 }
231 if let Some(dt) = value.get_datetime() {
232 return serde_json::Value::String(dt.to_string());
233 }
234 if let Some(dt_tz) = value.get_datetime_tz() {
235 return serde_json::Value::String(dt_tz.to_string());
236 }
237 if let Some(dec) = value.get_decimal() {
238 return serde_json::Value::String(dec.to_string());
239 }
240 if let Some(dur) = value.get_duration() {
241 return serde_json::Value::String(dur.to_string());
242 }
243 serde_json::Value::String(value.to_string())
245}
246
247#[cfg(test)]
248#[cfg(feature = "band8")]
249#[cfg_attr(coverage_nightly, coverage(off))]
250mod tests {
251 use std::future::Future;
252 use std::pin::Pin;
253 use std::sync::Arc;
254 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
255
256 use typedb_driver::IID;
257 use typedb_driver::concept::value::{Decimal, Duration, TimeZone};
258 use typedb_driver::concept::{
259 Attribute, AttributeType, Concept, Entity, EntityType, Value, ValueType,
260 };
261
262 use super::*;
263 use crate::error::PipelineError;
264 use crate::typedb::backend::TransactionOps;
265
266 struct MockTransaction {
271 query_result: Option<QueryResultKind>,
272 query_error: Option<String>,
273 commit_error: Option<String>,
274 committed: Arc<AtomicBool>,
275 query_called: Arc<AtomicBool>,
276 }
277
278 impl MockTransaction {
279 fn new(result: QueryResultKind) -> Self {
280 Self {
281 query_result: Some(result),
282 query_error: None,
283 commit_error: None,
284 committed: Arc::new(AtomicBool::new(false)),
285 query_called: Arc::new(AtomicBool::new(false)),
286 }
287 }
288
289 fn failing_query(msg: &str) -> Self {
290 Self {
291 query_result: None,
292 query_error: Some(msg.to_string()),
293 commit_error: None,
294 committed: Arc::new(AtomicBool::new(false)),
295 query_called: Arc::new(AtomicBool::new(false)),
296 }
297 }
298
299 fn with_commit_error(mut self, msg: &str) -> Self {
300 self.commit_error = Some(msg.to_string());
301 self
302 }
303 }
304
305 impl TransactionOps for MockTransaction {
306 fn query(
307 &mut self,
308 _typeql: &str,
309 ) -> Pin<Box<dyn Future<Output = Result<QueryResultKind, PipelineError>> + Send + '_>>
310 {
311 self.query_called.store(true, Ordering::SeqCst);
312 let result = self.query_result.take();
313 let error = self.query_error.take();
314 Box::pin(async move {
315 if let Some(msg) = error {
316 return Err(PipelineError::QueryExecution(msg));
317 }
318 Ok(result.expect("MockTransaction::query called more than once"))
319 })
320 }
321
322 fn commit(
323 &mut self,
324 ) -> Pin<Box<dyn Future<Output = Result<(), PipelineError>> + Send + '_>> {
325 self.committed.store(true, Ordering::SeqCst);
326 let error = self.commit_error.take();
327 Box::pin(async move {
328 if let Some(msg) = error {
329 return Err(PipelineError::QueryExecution(msg));
330 }
331 Ok(())
332 })
333 }
334 }
335
336 struct MockBackend {
337 transaction: std::sync::Mutex<Option<MockTransaction>>,
338 open_error: Option<String>,
339 is_open: bool,
340 open_called: Arc<AtomicUsize>,
341 }
342
343 impl MockBackend {
344 fn new(tx: MockTransaction) -> Self {
345 Self {
346 transaction: std::sync::Mutex::new(Some(tx)),
347 open_error: None,
348 is_open: true,
349 open_called: Arc::new(AtomicUsize::new(0)),
350 }
351 }
352
353 fn failing(msg: &str) -> Self {
354 Self {
355 transaction: std::sync::Mutex::new(None),
356 open_error: Some(msg.to_string()),
357 is_open: true,
358 open_called: Arc::new(AtomicUsize::new(0)),
359 }
360 }
361 }
362
363 impl DriverBackend for MockBackend {
364 fn open_transaction(
365 &self,
366 _database: &str,
367 _tx_type: TransactionType,
368 ) -> Pin<Box<dyn Future<Output = Result<Box<dyn TransactionOps>, PipelineError>> + Send + '_>>
369 {
370 self.open_called.fetch_add(1, Ordering::SeqCst);
371 let tx = self.transaction.lock().unwrap().take();
372 let error = self.open_error.clone();
373 Box::pin(async move {
374 if let Some(msg) = error {
375 return Err(PipelineError::QueryExecution(msg));
376 }
377 Ok(
378 Box::new(tx.expect("MockBackend: no transaction configured"))
379 as Box<dyn TransactionOps>,
380 )
381 })
382 }
383
384 fn is_open(&self) -> bool {
385 self.is_open
386 }
387 }
388
389 fn make_client(backend: MockBackend) -> TypeDBClient {
390 TypeDBClient::with_backend(Box::new(backend))
391 }
392
393 #[test]
398 fn parse_transaction_type_read() {
399 let result = parse_transaction_type("read").unwrap();
400 assert_eq!(result, TransactionType::Read);
401 }
402
403 #[test]
404 fn parse_transaction_type_write() {
405 let result = parse_transaction_type("write").unwrap();
406 assert_eq!(result, TransactionType::Write);
407 }
408
409 #[test]
410 fn parse_transaction_type_schema() {
411 let result = parse_transaction_type("schema").unwrap();
412 assert_eq!(result, TransactionType::Schema);
413 }
414
415 #[test]
416 fn parse_transaction_type_unknown() {
417 let result = parse_transaction_type("unknown");
418 let err = result.unwrap_err();
419 assert!(
420 matches!(&err, PipelineError::QueryExecution(msg) if msg.contains("Unknown transaction type: unknown"))
421 );
422 }
423
424 #[test]
425 fn parse_transaction_type_empty() {
426 let result = parse_transaction_type("");
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn parse_transaction_type_case_sensitive() {
432 let result = parse_transaction_type("Read");
433 assert!(result.is_err());
434 }
435
436 #[tokio::test]
441 async fn execute_ok_read_no_commit() {
442 let tx = MockTransaction::new(QueryResultKind::Ok);
443 let committed = tx.committed.clone();
444 let client = make_client(MockBackend::new(tx));
445
446 let result = client
447 .execute("db", "match $x isa thing;", "read")
448 .await
449 .unwrap();
450 assert_eq!(result, serde_json::json!({"ok": true}));
451 assert!(!committed.load(Ordering::SeqCst));
452 }
453
454 #[tokio::test]
455 async fn execute_ok_write_commits() {
456 let tx = MockTransaction::new(QueryResultKind::Ok);
457 let committed = tx.committed.clone();
458 let client = make_client(MockBackend::new(tx));
459
460 let result = client
461 .execute("db", "insert $x isa thing;", "write")
462 .await
463 .unwrap();
464 assert_eq!(result, serde_json::json!({"ok": true}));
465 assert!(committed.load(Ordering::SeqCst));
466 }
467
468 #[tokio::test]
469 async fn execute_ok_schema_commits() {
470 let tx = MockTransaction::new(QueryResultKind::Ok);
471 let committed = tx.committed.clone();
472 let client = make_client(MockBackend::new(tx));
473
474 let result = client
475 .execute("db", "define entity thing;", "schema")
476 .await
477 .unwrap();
478 assert_eq!(result, serde_json::json!({"ok": true}));
479 assert!(committed.load(Ordering::SeqCst));
480 }
481
482 #[tokio::test]
483 async fn execute_rows_read_no_commit() {
484 let rows = vec![
485 serde_json::json!({"name": "Alice"}),
486 serde_json::json!({"name": "Bob"}),
487 ];
488 let tx = MockTransaction::new(QueryResultKind::Rows(rows.clone()));
489 let committed = tx.committed.clone();
490 let client = make_client(MockBackend::new(tx));
491
492 let result = client
493 .execute("db", "match $p isa person;", "read")
494 .await
495 .unwrap();
496 assert_eq!(result, serde_json::Value::Array(rows));
497 assert!(!committed.load(Ordering::SeqCst));
498 }
499
500 #[tokio::test]
501 async fn execute_rows_write_commits() {
502 let rows = vec![serde_json::json!({"id": 1})];
503 let tx = MockTransaction::new(QueryResultKind::Rows(rows));
504 let committed = tx.committed.clone();
505 let client = make_client(MockBackend::new(tx));
506
507 let result = client
508 .execute("db", "insert $x isa thing;", "write")
509 .await
510 .unwrap();
511 assert!(result.is_array());
512 assert!(committed.load(Ordering::SeqCst));
513 }
514
515 #[tokio::test]
516 async fn execute_rows_data_preserved() {
517 let rows = vec![
518 serde_json::json!({"name": "Alice", "age": 30}),
519 serde_json::json!({"name": "Bob", "age": 25}),
520 ];
521 let tx = MockTransaction::new(QueryResultKind::Rows(rows.clone()));
522 let client = make_client(MockBackend::new(tx));
523
524 let result = client
525 .execute("db", "match $p isa person;", "read")
526 .await
527 .unwrap();
528 let arr = result.as_array().unwrap();
529 assert_eq!(arr.len(), 2);
530 assert_eq!(arr[0]["name"], "Alice");
531 assert_eq!(arr[1]["age"], 25);
532 }
533
534 #[tokio::test]
535 async fn execute_docs_read_no_commit() {
536 let docs = vec![serde_json::json!({"doc": "data"})];
537 let tx = MockTransaction::new(QueryResultKind::Documents(docs.clone()));
538 let committed = tx.committed.clone();
539 let client = make_client(MockBackend::new(tx));
540
541 let result = client
542 .execute("db", "match $p isa person; fetch {};", "read")
543 .await
544 .unwrap();
545 assert_eq!(result, serde_json::Value::Array(docs));
546 assert!(!committed.load(Ordering::SeqCst));
547 }
548
549 #[tokio::test]
550 async fn execute_docs_write_commits() {
551 let docs = vec![serde_json::json!({"doc": "data"})];
552 let tx = MockTransaction::new(QueryResultKind::Documents(docs));
553 let committed = tx.committed.clone();
554 let client = make_client(MockBackend::new(tx));
555
556 let result = client
557 .execute("db", "insert $x isa thing;", "write")
558 .await
559 .unwrap();
560 assert!(result.is_array());
561 assert!(committed.load(Ordering::SeqCst));
562 }
563
564 #[tokio::test]
565 async fn execute_docs_commit_error_ignored() {
566 let docs = vec![serde_json::json!({"doc": "data"})];
567 let tx = MockTransaction::new(QueryResultKind::Documents(docs.clone()))
568 .with_commit_error("commit failed");
569 let client = make_client(MockBackend::new(tx));
570
571 let result = client
573 .execute("db", "insert $x isa thing;", "write")
574 .await
575 .unwrap();
576 assert_eq!(result, serde_json::Value::Array(docs));
577 }
578
579 #[tokio::test]
580 async fn execute_transaction_open_failure() {
581 let client = make_client(MockBackend::failing("connection refused"));
582
583 let result = client.execute("db", "match $x isa thing;", "read").await;
584 let err = result.unwrap_err();
585 assert!(
586 matches!(&err, PipelineError::QueryExecution(msg) if msg.contains("connection refused"))
587 );
588 }
589
590 #[tokio::test]
591 async fn execute_query_failure() {
592 let tx = MockTransaction::failing_query("syntax error");
593 let client = make_client(MockBackend::new(tx));
594
595 let result = client.execute("db", "bad query", "read").await;
596 let err = result.unwrap_err();
597 assert!(matches!(&err, PipelineError::QueryExecution(msg) if msg.contains("syntax error")));
598 }
599
600 #[tokio::test]
601 async fn execute_commit_failure_ok_propagated() {
602 let tx = MockTransaction::new(QueryResultKind::Ok).with_commit_error("commit failed");
603 let client = make_client(MockBackend::new(tx));
604
605 let result = client.execute("db", "insert $x isa thing;", "write").await;
607 let err = result.unwrap_err();
608 assert!(
609 matches!(&err, PipelineError::QueryExecution(msg) if msg.contains("commit failed"))
610 );
611 }
612
613 #[tokio::test]
614 async fn execute_commit_failure_rows_propagated() {
615 let tx =
616 MockTransaction::new(QueryResultKind::Rows(vec![])).with_commit_error("commit failed");
617 let client = make_client(MockBackend::new(tx));
618
619 let result = client.execute("db", "insert $x isa thing;", "write").await;
621 let err = result.unwrap_err();
622 assert!(
623 matches!(&err, PipelineError::QueryExecution(msg) if msg.contains("commit failed"))
624 );
625 }
626
627 #[tokio::test]
628 async fn execute_invalid_transaction_type() {
629 let tx = MockTransaction::new(QueryResultKind::Ok);
630 let backend = MockBackend::new(tx);
631 let open_called = backend.open_called.clone();
632 let client = make_client(backend);
633
634 let result = client.execute("db", "match $x;", "invalid").await;
635 assert!(result.is_err());
636 assert_eq!(open_called.load(Ordering::SeqCst), 0);
638 }
639
640 #[test]
641 fn is_connected_delegates_to_backend() {
642 let mut backend = MockBackend::new(MockTransaction::new(QueryResultKind::Ok));
643 backend.is_open = true;
644 let client = make_client(backend);
645 assert!(client.is_connected());
646 }
647
648 #[test]
649 fn is_connected_false_when_backend_closed() {
650 let mut backend = MockBackend::new(MockTransaction::new(QueryResultKind::Ok));
651 backend.is_open = false;
652 let client = make_client(backend);
653 assert!(!client.is_connected());
654 }
655
656 #[test]
661 fn value_to_json_boolean_true() {
662 let value = Value::Boolean(true);
663 let json = value_to_json_b8(&value);
664 assert_eq!(json, serde_json::Value::Bool(true));
665 }
666
667 #[test]
668 fn value_to_json_boolean_false() {
669 let value = Value::Boolean(false);
670 let json = value_to_json_b8(&value);
671 assert_eq!(json, serde_json::Value::Bool(false));
672 }
673
674 #[test]
675 fn value_to_json_integer() {
676 let value = Value::Integer(42);
677 let json = value_to_json_b8(&value);
678 assert_eq!(json, serde_json::json!(42));
679 }
680
681 #[test]
682 fn value_to_json_integer_negative() {
683 let value = Value::Integer(-100);
684 let json = value_to_json_b8(&value);
685 assert_eq!(json, serde_json::json!(-100));
686 }
687
688 #[test]
689 fn value_to_json_double() {
690 let value = Value::Double(3.15);
691 let json = value_to_json_b8(&value);
692 assert_eq!(json, serde_json::json!(3.15));
693 }
694
695 #[test]
696 fn value_to_json_string() {
697 let value = Value::String("hello".to_string());
698 let json = value_to_json_b8(&value);
699 assert_eq!(json, serde_json::Value::String("hello".to_string()));
700 }
701
702 #[test]
703 fn value_to_json_string_empty() {
704 let value = Value::String(String::new());
705 let json = value_to_json_b8(&value);
706 assert_eq!(json, serde_json::Value::String(String::new()));
707 }
708
709 #[test]
710 fn value_to_json_date() {
711 let date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
712 let value = Value::Date(date);
713 let json = value_to_json_b8(&value);
714 assert_eq!(json, serde_json::Value::String("2024-01-15".to_string()));
715 }
716
717 #[test]
718 fn value_to_json_datetime() {
719 let dt = chrono::NaiveDate::from_ymd_opt(2024, 1, 15)
720 .unwrap()
721 .and_hms_opt(10, 30, 0)
722 .unwrap();
723 let value = Value::Datetime(dt);
724 let json = value_to_json_b8(&value);
725 let s = json.as_str().unwrap();
726 assert!(s.contains("2024-01-15"));
727 }
728
729 #[test]
730 fn value_to_json_decimal() {
731 let dec = Decimal::new(42, 0);
732 let value = Value::Decimal(dec);
733 let json = value_to_json_b8(&value);
734 assert!(json.is_string());
735 }
736
737 #[test]
738 fn value_to_json_duration() {
739 let dur = Duration::new(1, 2, 3_000_000_000);
740 let value = Value::Duration(dur);
741 let json = value_to_json_b8(&value);
742 assert!(json.is_string());
743 }
744
745 #[test]
746 fn value_to_json_datetime_tz() {
747 use chrono::TimeZone as _;
748 let tz = TimeZone::Fixed(chrono::FixedOffset::east_opt(3600).unwrap());
749 let dt = tz.with_ymd_and_hms(2024, 6, 15, 12, 30, 0).unwrap();
750 let value = Value::DatetimeTZ(dt);
751 let json = value_to_json_b8(&value);
752 let s = json.as_str().unwrap();
753 assert!(s.contains("2024"));
754 }
755
756 #[test]
761 fn concept_to_json_entity_type() {
762 let concept = Concept::EntityType(EntityType {
763 label: "person".to_string(),
764 });
765 let json = concept_to_json_b8(&concept);
766 assert_eq!(json["category"], "EntityType");
767 assert_eq!(json["label"], "person");
768 assert!(json.get("iid").is_none());
769 assert!(json.get("value").is_none());
770 }
771
772 #[test]
773 fn concept_to_json_attribute_type() {
774 let concept = Concept::AttributeType(AttributeType {
775 label: "name".to_string(),
776 value_type: Some(ValueType::String),
777 });
778 let json = concept_to_json_b8(&concept);
779 assert_eq!(json["category"], "AttributeType");
780 assert_eq!(json["label"], "name");
781 assert_eq!(json["value_type"], "string");
782 }
783
784 #[test]
785 fn concept_to_json_value_boolean() {
786 let concept = Concept::Value(Value::Boolean(true));
787 let json = concept_to_json_b8(&concept);
788 assert_eq!(json["category"], "Value");
789 assert_eq!(json["value"], true);
790 }
791
792 #[test]
793 fn concept_to_json_value_integer() {
794 let concept = Concept::Value(Value::Integer(42));
795 let json = concept_to_json_b8(&concept);
796 assert_eq!(json["value"], 42);
797 }
798
799 #[test]
800 fn concept_to_json_value_string() {
801 let concept = Concept::Value(Value::String("hello".to_string()));
802 let json = concept_to_json_b8(&concept);
803 assert_eq!(json["value"], "hello");
804 }
805
806 #[test]
807 fn concept_to_json_entity_with_iid() {
808 let iid: IID = vec![0x01, 0x02, 0x03].into();
809 let concept = Concept::Entity(Entity {
810 iid,
811 type_: Some(EntityType {
812 label: "person".to_string(),
813 }),
814 });
815 let json = concept_to_json_b8(&concept);
816 assert_eq!(json["category"], "Entity");
817 assert_eq!(json["label"], "person");
818 let iid_str = json["iid"].as_str().unwrap();
820 assert!(iid_str.starts_with("0x"));
821 }
822
823 #[test]
824 fn concept_to_json_attribute_with_value() {
825 let iid: IID = vec![0xAA, 0xBB].into();
826 let concept = Concept::Attribute(Attribute {
827 iid,
828 value: Value::String("hello".to_string()),
829 type_: Some(AttributeType {
830 label: "name".to_string(),
831 value_type: Some(ValueType::String),
832 }),
833 });
834 let json = concept_to_json_b8(&concept);
835 assert_eq!(json["category"], "Attribute");
836 assert_eq!(json["label"], "name");
837 assert!(json.get("iid").is_none());
839 assert_eq!(json["value"], "hello");
840 assert_eq!(json["value_type"], "string");
841 }
842
843 #[test]
844 fn concept_to_json_attribute_type_without_value_type() {
845 let concept = Concept::AttributeType(AttributeType {
846 label: "abstract_attr".to_string(),
847 value_type: None,
848 });
849 let json = concept_to_json_b8(&concept);
850 assert_eq!(json["label"], "abstract_attr");
851 assert!(json.get("value_type").is_none());
852 }
853
854 #[test]
859 fn type_db_client_implements_query_executor() {
860 fn assert_executor<T: QueryExecutor>() {}
861 assert_executor::<TypeDBClient>();
862 }
863
864 #[tokio::test]
865 async fn query_executor_execute_delegates_to_client() {
866 let tx = MockTransaction::new(QueryResultKind::Rows(vec![serde_json::json!({"x": 1})]));
867 let client = make_client(MockBackend::new(tx));
868 let executor: Box<dyn QueryExecutor> = Box::new(client);
869
870 let result = executor
871 .execute("db", "match $x isa thing;", "read")
872 .await
873 .unwrap();
874 assert!(result.is_array());
875 assert_eq!(result.as_array().unwrap().len(), 1);
876 }
877
878 #[test]
879 fn query_executor_is_connected_delegates_to_client() {
880 let mut backend = MockBackend::new(MockTransaction::new(QueryResultKind::Ok));
881 backend.is_open = true;
882 let client = make_client(backend);
883 let executor: Box<dyn QueryExecutor> = Box::new(client);
884 assert!(executor.is_connected());
885 }
886
887 #[tokio::test]
892 #[ignore = "requires running TypeDB server"]
893 #[cfg_attr(coverage_nightly, coverage(off))]
894 async fn integration_connect_invalid_address() {
895 let config = TypeDBSection {
896 address: "localhost:99999".to_string(),
897 database: "test".to_string(),
898 username: "admin".to_string(),
899 password: "password".to_string(),
900 http_port: 8000,
901 };
902 let result = TypeDBClient::connect(&config).await;
903 assert!(result.is_err());
904 }
905
906 fn live_config() -> TypeDBSection {
909 TypeDBSection {
910 address: std::env::var("TYPEDB_ADDRESS")
911 .unwrap_or_else(|_| "localhost:1729".to_string()),
912 database: std::env::var("TYPEDB_DATABASE").unwrap_or_else(|_| "test".to_string()),
913 username: "admin".to_string(),
914 password: "password".to_string(),
915 http_port: std::env::var("TYPEDB_HTTP_PORT")
916 .ok()
917 .and_then(|port| port.parse().ok())
918 .unwrap_or(8000),
919 }
920 }
921
922 #[tokio::test]
923 #[ignore = "requires running TypeDB server"]
924 #[cfg_attr(coverage_nightly, coverage(off))]
925 async fn integration_connect_success() {
926 let result = TypeDBClient::connect(&live_config()).await;
927 assert!(result.is_ok());
928 assert!(result.unwrap().is_connected());
929 }
930
931 #[tokio::test]
932 #[ignore = "requires running TypeDB server"]
933 #[cfg_attr(coverage_nightly, coverage(off))]
934 async fn integration_execute_roundtrip() {
935 let config = live_config();
936 let client = TypeDBClient::connect(&config)
937 .await
938 .expect("connect failed");
939
940 client
941 .execute(&config.database, "define entity smoke_marker;", "schema")
942 .await
943 .expect("schema define failed");
944
945 let rows = client
946 .execute(&config.database, "match entity $t;", "read")
947 .await
948 .expect("read query failed");
949 let rows = rows.as_array().expect("read result must be a JSON array");
950 assert!(
951 !rows.is_empty(),
952 "expected at least the smoke_marker entity type"
953 );
954 }
955}