tegdb 0.5.0

The name TegridyDB (short for TegDB) is inspired by the Tegridy Farm in South Park and tries to correct some of the wrong database implementations, such as null support, implicit conversion support, etc.
Documentation
//! Verification script for chatbot examples
//!
//! This script verifies that:
//! 1. TegDB EMBED() function works
//! 2. Vector search works
//! 3. Ollama API is accessible
//! 4. gemma3:latest model works
//!
//! Run: cargo run --example verify_chatbot_setup

use serde_json::json;
use tegdb::{Database, Result, SqlValue};

#[tokio::main]
async fn main() -> Result<()> {
    println!("🔍 Verifying Chatbot Setup");
    println!("==========================\n");

    let mut all_ok = true;

    // Test 1: TegDB EMBED() function
    print!("1. Testing TegDB EMBED() function... ");
    match test_embed_function() {
        Ok(_) => println!("✅ PASS"),
        Err(e) => {
            println!("❌ FAIL: {e}");
            all_ok = false;
        }
    }

    // Test 2: Vector search with COSINE_SIMILARITY
    print!("2. Testing vector search... ");
    match test_vector_search() {
        Ok(_) => println!("✅ PASS"),
        Err(e) => {
            println!("❌ FAIL: {e}");
            all_ok = false;
        }
    }

    // Test 3: Ollama connection
    print!("3. Testing Ollama connection... ");
    match test_ollama_connection().await {
        Ok(_) => println!("✅ PASS"),
        Err(e) => {
            println!("❌ FAIL: {e}");
            all_ok = false;
        }
    }

    // Test 4: gemma3:latest model
    print!("4. Testing gemma3:latest model... ");
    match test_gemma3_model().await {
        Ok(_) => println!("✅ PASS"),
        Err(e) => {
            println!("❌ FAIL: {e}");
            all_ok = false;
        }
    }

    // Test 5: End-to-end chatbot functionality
    print!("5. Testing end-to-end chatbot flow... ");
    match test_chatbot_flow().await {
        Ok(_) => println!("✅ PASS"),
        Err(e) => {
            println!("❌ FAIL: {e}");
            all_ok = false;
        }
    }

    println!("\n================================");
    if all_ok {
        println!("✅ All tests passed!");
        println!("\nYou can now run:");
        println!("  cargo run --example ollama_chatbot_simple");
        println!("  cargo run --example ollama_chatbot_demo");
    } else {
        println!("❌ Some tests failed. Please check the errors above.");
    }

    Ok(())
}

fn test_embed_function() -> Result<()> {
    let db_path = std::env::temp_dir()
        .join("test_embed")
        .with_extension("teg");
    let _ = std::fs::remove_file(&db_path);
    let mut db = Database::open(db_path.to_string_lossy())?;

    // Test EMBED() in SELECT
    let result = db.query("SELECT EMBED('test') as embedding")?;
    let rows = result.rows();

    if rows.is_empty() {
        return Err(tegdb::Error::Other("No rows returned".to_string()));
    }

    match &rows[0][0] {
        SqlValue::Vector(v) => {
            if v.len() != 128 {
                return Err(tegdb::Error::Other(format!(
                    "Expected 128 dimensions, got {}",
                    v.len()
                )));
            }
        }
        _ => return Err(tegdb::Error::Other("Expected vector value".to_string())),
    }

    Ok(())
}

fn test_vector_search() -> Result<()> {
    let db_path = std::env::temp_dir()
        .join("test_search")
        .with_extension("teg");
    let _ = std::fs::remove_file(&db_path);
    let mut db = Database::open(db_path.to_string_lossy())?;

    // Create table and insert test data
    db.execute(
        "CREATE TABLE test (id INTEGER PRIMARY KEY, text TEXT(100), embedding VECTOR(128))",
    )?;

    db.execute("INSERT INTO test VALUES (1, 'hello', EMBED('hello'))")?;
    db.execute("INSERT INTO test VALUES (2, 'world', EMBED('world'))")?;
    db.execute("INSERT INTO test VALUES (3, 'hello world', EMBED('hello world'))")?;

    // Test similarity search
    let result = db.query(
        "SELECT text, COSINE_SIMILARITY(embedding, EMBED('hello')) as sim 
         FROM test 
         ORDER BY sim DESC 
         LIMIT 1",
    )?;

    let rows = result.rows();
    if rows.is_empty() {
        return Err(tegdb::Error::Other("No search results".to_string()));
    }

    // Check that we got a similarity score
    match &rows[0][1] {
        SqlValue::Real(sim) => {
            if *sim < 0.0 || *sim > 1.0 {
                return Err(tegdb::Error::Other(format!(
                    "Invalid similarity score: {sim}"
                )));
            }
        }
        _ => {
            return Err(tegdb::Error::Other(
                "Expected real value for similarity".to_string(),
            ))
        }
    }

    Ok(())
}

async fn test_ollama_connection() -> Result<()> {
    let client = reqwest::Client::new();
    let response = client
        .get("http://localhost:11434/api/tags")
        .timeout(std::time::Duration::from_secs(5))
        .send()
        .await
        .map_err(|e| tegdb::Error::Other(format!("Connection failed: {e}")))?;

    if !response.status().is_success() {
        return Err(tegdb::Error::Other(format!(
            "Ollama returned error: {}",
            response.status()
        )));
    }

    Ok(())
}

async fn test_gemma3_model() -> Result<()> {
    let client = reqwest::Client::new();

    let payload = json!({
        "model": "gemma3:latest",
        "messages": [
            {"role": "user", "content": "Say 'test' and nothing else"}
        ],
        "stream": false
    });

    let response = client
        .post("http://localhost:11434/api/chat")
        .json(&payload)
        .timeout(std::time::Duration::from_secs(30))
        .send()
        .await
        .map_err(|e| tegdb::Error::Other(format!("Request failed: {e}")))?;

    if !response.status().is_success() {
        return Err(tegdb::Error::Other(format!(
            "Model returned error: {}",
            response.status()
        )));
    }

    let json: serde_json::Value = response
        .json()
        .await
        .map_err(|e| tegdb::Error::Other(format!("Parse failed: {e}")))?;

    // Check if we got a valid response
    if json["message"]["content"].as_str().is_none() {
        return Err(tegdb::Error::Other("No content in response".to_string()));
    }

    Ok(())
}

async fn test_chatbot_flow() -> Result<()> {
    let db_path = std::env::temp_dir()
        .join("test_chatbot")
        .with_extension("teg");
    let _ = std::fs::remove_file(&db_path);
    let mut db = Database::open(db_path.to_string_lossy())?;

    // Setup chatbot database
    db.execute(
        "CREATE TABLE chat (id INTEGER PRIMARY KEY, message TEXT(500), embedding VECTOR(128))",
    )?;

    // Store a message with embedding
    db.execute("INSERT INTO chat VALUES (1, 'What is AI?', EMBED('What is AI?'))")?;

    // Search for similar message
    let result = db.query(
        "SELECT message FROM chat 
         ORDER BY COSINE_SIMILARITY(embedding, EMBED('What is artificial intelligence?')) DESC 
         LIMIT 1",
    )?;

    if result.rows().is_empty() {
        return Err(tegdb::Error::Other(
            "Search returned no results".to_string(),
        ));
    }

    // Test Ollama response
    let client = reqwest::Client::new();
    let payload = json!({
        "model": "gemma3:latest",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "What is AI? Answer in 10 words or less."}
        ],
        "stream": false
    });

    let response = client
        .post("http://localhost:11434/api/chat")
        .json(&payload)
        .timeout(std::time::Duration::from_secs(30))
        .send()
        .await
        .map_err(|e| tegdb::Error::Other(format!("Ollama request failed: {e}")))?;

    let json: serde_json::Value = response
        .json()
        .await
        .map_err(|e| tegdb::Error::Other(format!("Response parse failed: {e}")))?;

    let content = json["message"]["content"]
        .as_str()
        .ok_or_else(|| tegdb::Error::Other("No response content".to_string()))?;

    // Store response
    let escaped_content = content.replace('\'', "''");
    db.execute(&format!(
        "INSERT INTO chat VALUES (2, '{escaped_content}', EMBED('What is AI?'))"
    ))?;

    // Verify both messages stored
    let count_result = db.query("SELECT COUNT(*) FROM chat")?;
    match &count_result.rows()[0][0] {
        SqlValue::Integer(count) => {
            if *count != 2 {
                return Err(tegdb::Error::Other(format!(
                    "Expected 2 messages, found {count}"
                )));
            }
        }
        _ => return Err(tegdb::Error::Other("Invalid count result".to_string())),
    }

    Ok(())
}