Skip to main content

type_bridge_server/typedb/
client.rs

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