flowcode-core 0.4.3-alpha

Core execution engine for FlowCode data scripting language
Documentation
//! Integration tests for DuckDB functionality in FlowCode

#[cfg(feature = "duckdb")]
mod duckdb_tests {
    use std::error::Error;
    use std::sync::Arc;
    
    use flowcode_core::duckdb_interchange::{record_batch_to_table, table_to_record_batch};
    use flowcode_core::duckdb_session::DuckSession;
    use flowcode_core::types::{TypedValue, ValueKind};
    use flowcode_core::ast::ArgValue;
    
    // Import Arrow types through duckdb's re-export to ensure version compatibility
    use duckdb::arrow::array::*;
    use duckdb::arrow::datatypes::*;
    use duckdb::arrow::record_batch::RecordBatch;
    
    // Helper function to create a test record batch
    fn create_test_record_batch() -> Result<RecordBatch, Box<dyn Error>> {
        let schema = Schema::new(vec![
            Field::new("id", DataType::Int32, false),
            Field::new("name", DataType::Utf8, false),
            Field::new("value", DataType::Float64, true),
            Field::new("active", DataType::Boolean, true),
        ]);
        
        let id_array = Int32Array::from(vec![1, 2, 3]);
        let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
        let value_array = Float64Array::from(vec![Some(1.1), None, Some(3.3)]);
        let active_array = BooleanArray::from(vec![Some(true), Some(false), None]);
        
        RecordBatch::try_new(
            Arc::new(schema),
            vec![
                Arc::new(id_array),
                Arc::new(name_array),
                Arc::new(value_array),
                Arc::new(active_array),
            ]
        ).map_err(Into::into)
    }

    #[test]
    fn test_record_batch_to_table() -> Result<(), Box<dyn Error>> {
        // Create a test record batch using our helper function
        let batch = create_test_record_batch()?;

        // Convert to FlowCode Table
        let table = record_batch_to_table(batch)?;
        
        // Verify the structure
        if let ArgValue::Table(rows) = &table.value {
            // Check row and column counts
            assert_eq!(rows.len(), 3); // 3 rows
            assert_eq!(rows[0].len(), 4); // 4 columns
            
            // Check first row values
            if let ArgValue::Number(n) = rows[0][0].value {
                assert_eq!(n, 1.0);
            } else {
                panic!("Expected number for id");
            }
            
            if let ArgValue::String(s) = &rows[0][1].value {
                assert_eq!(s, "Alice");
            } else {
                panic!("Expected string for name");
            }
            
            // Check NULL handling
            assert_eq!(rows[1][2].kind, ValueKind::Null); // NULL value
            assert_eq!(rows[2][3].kind, ValueKind::Null); // NULL active
            
            // Check type preservation
            assert_eq!(rows[0][0].kind, ValueKind::Number);
            assert_eq!(rows[0][1].kind, ValueKind::String);
            assert_eq!(rows[0][2].kind, ValueKind::Number);
            assert_eq!(rows[0][3].kind, ValueKind::Bool);
        } else {
            panic!("Expected Table variant");
        }
        
        Ok(())
    }

    #[test]
    fn test_table_to_record_batch() -> Result<(), Box<dyn Error>> {
        // Create a FlowCode Table with various data types
        let table = TypedValue::new(
            ValueKind::Table,
            ArgValue::Table(vec![
                // First row
                vec![
                    TypedValue::new(ValueKind::Number, ArgValue::Number(1.0)),
                    TypedValue::new(ValueKind::String, ArgValue::String("Alice".to_string())),
                    TypedValue::new(ValueKind::Number, ArgValue::Number(42.5)),
                    TypedValue::new(ValueKind::Bool, ArgValue::Bool(true)),
                ],
                // Second row with some NULLs
                vec![
                    TypedValue::new(ValueKind::Number, ArgValue::Number(2.0)),
                    TypedValue::new(ValueKind::String, ArgValue::String("Bob".to_string())),
                    TypedValue::new(ValueKind::Null, ArgValue::Null),
                    TypedValue::new(ValueKind::Bool, ArgValue::Bool(false)),
                ],
                // Third row with different types
                vec![
                    TypedValue::new(ValueKind::Number, ArgValue::Number(3.0)),
                    TypedValue::new(ValueKind::String, ArgValue::String("Charlie".to_string())),
                    TypedValue::new(ValueKind::Number, ArgValue::Number(3.14)),
                    TypedValue::new(ValueKind::Null, ArgValue::Null),
                ],
            ]),
        );

        // Convert to RecordBatch
        let batch = table_to_record_batch(&table)?;
        
        // Verify the structure
        assert_eq!(batch.num_columns(), 4, "Should have 4 columns");
        assert_eq!(batch.num_rows(), 3, "Should have 3 rows");
        
        // Check column types (should be inferred from non-null values)
        assert_eq!(batch.schema().field(0).data_type(), &DataType::Float64, "First column should be Float64");
        assert_eq!(batch.schema().field(1).data_type(), &DataType::Utf8, "Second column should be Utf8");
        assert_eq!(batch.schema().field(2).data_type(), &DataType::Float64, "Third column should be Float64");
        assert_eq!(batch.schema().field(3).data_type(), &DataType::Boolean, "Fourth column should be Boolean");
        
        // Verify data values
        let cols = batch.columns();
        
        // Check first column (numbers)
        let col0 = cols[0].as_any().downcast_ref::<Float64Array>().unwrap();
        assert_eq!(col0.value(0), 1.0);
        assert_eq!(col0.value(1), 2.0);
        assert_eq!(col0.value(2), 3.0);
        
        // Check second column (strings)
        let col1 = cols[1].as_any().downcast_ref::<StringArray>().unwrap();
        assert_eq!(col1.value(0), "Alice");
        assert_eq!(col1.value(1), "Bob");
        assert_eq!(col1.value(2), "Charlie");
        
        // Check third column (numbers with NULL)
        let col2 = cols[2].as_any().downcast_ref::<Float64Array>().unwrap();
        assert_eq!(col2.value(0), 42.5);
        assert!(col2.is_null(1));
        assert_eq!(col2.value(2), 3.14);
        
        // Check fourth column (booleans with NULL)
        let col3 = cols[3].as_any().downcast_ref::<BooleanArray>().unwrap();
        assert_eq!(col3.value(0), true);
        assert_eq!(col3.value(1), false);
        assert!(col3.is_null(2));
        
        Ok(())
    }

    #[test]
    fn test_duckdb_session_basic() -> Result<(), Box<dyn Error>> {
        let session = DuckSession::new()?;
        
        // Test a simple query
        let result = session.execute_sql("SELECT 1 as one, 'test' as two, true as three")?;
        
        if let ArgValue::Table(rows) = &result.value {
            assert_eq!(rows.len(), 1);
            assert_eq!(rows[0].len(), 3);
            
            // Check the values
            if let ArgValue::Number(n) = rows[0][0].value {
                assert_eq!(n, 1.0);
            } else {
                panic!("Expected number");
            }
            
            if let ArgValue::String(s) = &rows[0][1].value {
                assert_eq!(s, "test");
            } else {
                panic!("Expected string");
            }
            
            if let ArgValue::Bool(b) = rows[0][2].value {
                assert!(b);
            } else {
                panic!("Expected bool");
            }
        } else {
            panic!("Expected Table variant");
        }
        
        // Test with parameters
        let result = session.execute_with_params(
            "SELECT ? as num, ? as str, ? as flag",
            &[
                ArgValue::Number(42.0),
                ArgValue::String("param".to_string()),
                ArgValue::Bool(true),
            ],
        )?;
        
        if let ArgValue::Table(rows) = &result.value {
            assert_eq!(rows.len(), 1);
            assert_eq!(rows[0].len(), 3);
        }
        
        Ok(())
    }

    #[test]
    fn test_execute_query() -> Result<(), Box<dyn Error>> {
        // Create a new DuckDB session
        let session = DuckSession::new()?;
        
        // Create a sample table
        let create_table = "CREATE TABLE test_table (id INTEGER, name VARCHAR);";
        session.execute_sql(create_table)?;
        
        // Insert data
        let insert_data = "INSERT INTO test_table VALUES (1, 'Alice'), (2, 'Bob');";
        session.execute_sql(insert_data)?;
        
        // Query the table
        let query = "SELECT * FROM test_table ORDER BY id;";
        let result = session.execute_sql(query)?;
        
        // Check results
        if let ArgValue::Table(rows) = &result.value {
            assert_eq!(rows.len(), 2, "Expected 2 rows");
            // Check first row
            if let ArgValue::Number(id) = rows[0][0].value {
                assert_eq!(id, 1.0, "Expected id=1");
            } else {
                panic!("Expected number for id");
            }
            if let ArgValue::String(name) = &rows[0][1].value {
                assert_eq!(name, "Alice", "Expected name=Alice");
            } else {
                panic!("Expected string for name");
            }
        } else {
            panic!("Expected table result");
        }
        
        Ok(())
    }

    #[test]
    fn test_duckdb_csv_handling() -> Result<(), Box<dyn Error>> {
        use std::io::Write;
        use tempfile::Builder;

        // 1. Create and write the CSV
        let mut tmp = Builder::new()
            .suffix(".csv")
            .tempfile()?;
        writeln!(tmp, "id,name")?;
        writeln!(tmp, "1,Alice")?;
        writeln!(tmp, "2,Bob")?;
        tmp.flush()?; // Make sure bytes are on disk
        let path = tmp.into_temp_path(); // Closes handle but leaves file on disk
        let path_str = path.to_str().unwrap();

        // 2. Now load it
        let sess = DuckSession::new()?;
        sess.load_csv_auto("people", path_str)?;

        // 3. Query the loaded data
        let result = sess.execute_sql("SELECT * FROM people")?;
        if let ArgValue::Table(rows) = &result.value {
            assert_eq!(rows.len(), 2, "Expected 2 rows in the result");
        } else {
            panic!("Expected a Table result");
        }

        Ok(())
    }
}