use recoco::base::schema::{BasicValueType, EnrichedValueType, FieldSchema, ValueType};
use recoco::base::value::{BasicValue, FieldValues, KeyPart, Value};
use recoco::ops::factory_bases::TargetFactoryBase;
use recoco::setup::{ResourceSetupChange, SetupChangeType};
use thread_flow::targets::d1::{
D1ExportContext, D1SetupChange, D1SetupState, D1TableId, D1TargetFactory, IndexSchema,
basic_value_to_json, key_part_to_json, value_to_json, value_type_to_sql,
};
fn test_field_schema(name: &str, typ: BasicValueType, nullable: bool) -> FieldSchema {
FieldSchema::new(
name,
EnrichedValueType {
typ: ValueType::Basic(typ),
nullable,
attrs: Default::default(),
},
)
}
fn test_table_id() -> D1TableId {
D1TableId {
database_id: "test-db-456".to_string(),
table_name: "test_table".to_string(),
}
}
#[test]
fn test_key_part_to_json_str() {
let key_part = KeyPart::Str("test_string".into());
let json = key_part_to_json(&key_part).expect("Failed to convert str");
assert_eq!(json, serde_json::json!("test_string"));
}
#[test]
fn test_key_part_to_json_bool() {
let key_part_true = KeyPart::Bool(true);
let json_true = key_part_to_json(&key_part_true).expect("Failed to convert bool");
assert_eq!(json_true, serde_json::json!(true));
let key_part_false = KeyPart::Bool(false);
let json_false = key_part_to_json(&key_part_false).expect("Failed to convert bool");
assert_eq!(json_false, serde_json::json!(false));
}
#[test]
fn test_key_part_to_json_int64() {
let key_part = KeyPart::Int64(42);
let json = key_part_to_json(&key_part).expect("Failed to convert int64");
assert_eq!(json, serde_json::json!(42));
let key_part_negative = KeyPart::Int64(-100);
let json_negative =
key_part_to_json(&key_part_negative).expect("Failed to convert negative int64");
assert_eq!(json_negative, serde_json::json!(-100));
}
#[test]
fn test_basic_value_to_json_bool() {
let value = BasicValue::Bool(true);
let json = basic_value_to_json(&value).expect("Failed to convert bool");
assert_eq!(json, serde_json::json!(true));
}
#[test]
fn test_basic_value_to_json_int64() {
let value = BasicValue::Int64(9999);
let json = basic_value_to_json(&value).expect("Failed to convert int64");
assert_eq!(json, serde_json::json!(9999));
}
#[test]
fn test_basic_value_to_json_float32() {
let value = BasicValue::Float32(std::f32::consts::PI);
let json = basic_value_to_json(&value).expect("Failed to convert float32");
assert!(json.is_number());
let nan_value = BasicValue::Float32(f32::NAN);
let json_nan = basic_value_to_json(&nan_value).expect("Failed to convert NaN");
assert_eq!(json_nan, serde_json::json!(null));
}
#[test]
fn test_basic_value_to_json_float64() {
let value = BasicValue::Float64(std::f64::consts::E);
let json = basic_value_to_json(&value).expect("Failed to convert float64");
assert!(json.is_number());
let inf_value = BasicValue::Float64(f64::INFINITY);
let json_inf = basic_value_to_json(&inf_value).expect("Failed to convert infinity");
assert_eq!(json_inf, serde_json::json!(null));
}
#[test]
fn test_basic_value_to_json_str() {
let value = BasicValue::Str("hello world".into());
let json = basic_value_to_json(&value).expect("Failed to convert str");
assert_eq!(json, serde_json::json!("hello world"));
}
#[test]
fn test_value_to_json_null() {
let value = Value::Null;
let json = value_to_json(&value).expect("Failed to convert null");
assert_eq!(json, serde_json::json!(null));
}
#[test]
fn test_value_to_json_basic() {
let value = Value::Basic(BasicValue::Str("test".into()));
let json = value_to_json(&value).expect("Failed to convert basic value");
assert_eq!(json, serde_json::json!("test"));
}
#[test]
fn test_value_to_json_struct() {
let field_values = FieldValues {
fields: vec![
Value::Basic(BasicValue::Str("field1".into())),
Value::Basic(BasicValue::Int64(42)),
],
};
let value = Value::Struct(field_values);
let json = value_to_json(&value).expect("Failed to convert struct");
assert_eq!(json, serde_json::json!(["field1", 42]));
}
#[test]
fn test_value_type_to_sql_bool() {
let typ = ValueType::Basic(BasicValueType::Bool);
assert_eq!(value_type_to_sql(&typ), "INTEGER");
}
#[test]
fn test_value_type_to_sql_int64() {
let typ = ValueType::Basic(BasicValueType::Int64);
assert_eq!(value_type_to_sql(&typ), "INTEGER");
}
#[test]
fn test_value_type_to_sql_float() {
let typ32 = ValueType::Basic(BasicValueType::Float32);
assert_eq!(value_type_to_sql(&typ32), "REAL");
let typ64 = ValueType::Basic(BasicValueType::Float64);
assert_eq!(value_type_to_sql(&typ64), "REAL");
}
#[test]
fn test_value_type_to_sql_str() {
let typ = ValueType::Basic(BasicValueType::Str);
assert_eq!(value_type_to_sql(&typ), "TEXT");
}
#[test]
fn test_value_type_to_sql_json() {
let typ = ValueType::Basic(BasicValueType::Json);
assert_eq!(value_type_to_sql(&typ), "TEXT");
}
#[test]
fn test_create_table_sql_simple() {
let key_fields = vec![test_field_schema("id", BasicValueType::Int64, false)];
let value_fields = vec![
test_field_schema("name", BasicValueType::Str, false),
test_field_schema("age", BasicValueType::Int64, true),
];
let state = D1SetupState::new(&test_table_id(), &key_fields, &value_fields)
.expect("Failed to create setup state");
let sql = state.create_table_sql();
assert!(sql.contains("CREATE TABLE IF NOT EXISTS test_table"));
assert!(sql.contains("id INTEGER NOT NULL"));
assert!(sql.contains("name TEXT NOT NULL"));
assert!(sql.contains("age INTEGER"));
assert!(!sql.contains("age INTEGER NOT NULL")); assert!(sql.contains("PRIMARY KEY (id)"));
}
#[test]
fn test_create_table_sql_composite_key() {
let key_fields = vec![
test_field_schema("tenant_id", BasicValueType::Str, false),
test_field_schema("user_id", BasicValueType::Int64, false),
];
let value_fields = vec![test_field_schema("email", BasicValueType::Str, false)];
let state = D1SetupState::new(&test_table_id(), &key_fields, &value_fields)
.expect("Failed to create setup state");
let sql = state.create_table_sql();
assert!(sql.contains("tenant_id TEXT NOT NULL"));
assert!(sql.contains("user_id INTEGER NOT NULL"));
assert!(sql.contains("PRIMARY KEY (tenant_id, user_id)"));
}
#[test]
fn test_create_indexes_sql_unique() {
let state = D1SetupState {
table_id: test_table_id(),
key_columns: vec![],
value_columns: vec![],
indexes: vec![IndexSchema {
name: "idx_unique_email".to_string(),
columns: vec!["email".to_string()],
unique: true,
}],
};
let sqls = state.create_indexes_sql();
assert_eq!(sqls.len(), 1);
assert!(sqls[0].contains("CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_email"));
assert!(sqls[0].contains("ON test_table (email)"));
}
#[test]
fn test_create_indexes_sql_composite() {
let state = D1SetupState {
table_id: test_table_id(),
key_columns: vec![],
value_columns: vec![],
indexes: vec![IndexSchema {
name: "idx_tenant_user".to_string(),
columns: vec!["tenant_id".to_string(), "user_id".to_string()],
unique: false,
}],
};
let sqls = state.create_indexes_sql();
assert_eq!(sqls.len(), 1);
assert!(sqls[0].contains("ON test_table (tenant_id, user_id)"));
}
#[test]
fn test_d1_setup_state_new() {
let key_fields = vec![test_field_schema("id", BasicValueType::Int64, false)];
let value_fields = vec![
test_field_schema("name", BasicValueType::Str, false),
test_field_schema("score", BasicValueType::Float64, true),
];
let state = D1SetupState::new(&test_table_id(), &key_fields, &value_fields)
.expect("Failed to create setup state");
assert_eq!(state.table_id, test_table_id());
assert_eq!(state.key_columns.len(), 1);
assert_eq!(state.key_columns[0].name, "id");
assert_eq!(state.key_columns[0].sql_type, "INTEGER");
assert!(state.key_columns[0].primary_key);
assert!(!state.key_columns[0].nullable);
assert_eq!(state.value_columns.len(), 2);
assert_eq!(state.value_columns[0].name, "name");
assert!(!state.value_columns[0].primary_key);
assert_eq!(state.value_columns[1].name, "score");
assert!(state.value_columns[1].nullable);
}
#[test]
fn test_d1_setup_change_describe_changes_create() {
let change = D1SetupChange {
table_id: test_table_id(),
create_table_sql: Some("CREATE TABLE test_table (id INTEGER)".to_string()),
create_indexes_sql: vec!["CREATE INDEX idx_id ON test_table (id)".to_string()],
alter_table_sql: vec![],
};
let descriptions = change.describe_changes();
assert_eq!(descriptions.len(), 2);
let desc_strings: Vec<String> = descriptions
.iter()
.map(|d| match d {
recoco::setup::ChangeDescription::Action(s) => s.clone(),
_ => String::new(),
})
.collect();
assert!(desc_strings.iter().any(|s| s.contains("CREATE TABLE")));
assert!(desc_strings.iter().any(|s| s.contains("CREATE INDEX")));
}
#[test]
fn test_d1_setup_change_type_create() {
let change = D1SetupChange {
table_id: test_table_id(),
create_table_sql: Some("CREATE TABLE test_table (id INTEGER)".to_string()),
create_indexes_sql: vec![],
alter_table_sql: vec![],
};
assert_eq!(change.change_type(), SetupChangeType::Create);
}
#[test]
fn test_d1_setup_change_type_update() {
let change = D1SetupChange {
table_id: test_table_id(),
create_table_sql: None,
create_indexes_sql: vec!["CREATE INDEX idx ON test_table (col)".to_string()],
alter_table_sql: vec![],
};
assert_eq!(change.change_type(), SetupChangeType::Update);
}
#[test]
fn test_d1_setup_change_type_invalid() {
let change = D1SetupChange {
table_id: test_table_id(),
create_table_sql: None,
create_indexes_sql: vec![],
alter_table_sql: vec![],
};
assert_eq!(change.change_type(), SetupChangeType::Invalid);
}
#[test]
fn test_factory_name() {
let factory = D1TargetFactory;
assert_eq!(factory.name(), "d1");
}
#[test]
fn test_describe_resource() {
let factory = D1TargetFactory;
let table_id = D1TableId {
database_id: "my-database".to_string(),
table_name: "my_table".to_string(),
};
let description = factory
.describe_resource(&table_id)
.expect("Failed to describe resource");
assert_eq!(description, "D1 table: my-database.my_table");
}
#[test]
fn test_d1_export_context_new() {
let key_fields = vec![test_field_schema("id", BasicValueType::Int64, false)];
let value_fields = vec![test_field_schema("name", BasicValueType::Str, false)];
let metrics = thread_flow::monitoring::performance::PerformanceMetrics::new();
let context = D1ExportContext::new_with_default_client(
"test-db".to_string(),
"test_table".to_string(),
"test-account".to_string(),
"test-token".to_string(),
key_fields.clone(),
value_fields.clone(),
metrics,
);
assert!(context.is_ok());
let context = context.unwrap();
assert_eq!(context.database_id, "test-db");
assert_eq!(context.table_name, "test_table");
assert_eq!(context.account_id, "test-account");
assert_eq!(context.api_token, "test-token");
assert_eq!(context.key_fields_schema.len(), 1);
assert_eq!(context.value_fields_schema.len(), 1);
}
#[test]
fn test_d1_export_context_api_url() {
let key_fields = vec![test_field_schema("id", BasicValueType::Int64, false)];
let value_fields = vec![test_field_schema("name", BasicValueType::Str, false)];
let metrics = thread_flow::monitoring::performance::PerformanceMetrics::new();
let context = D1ExportContext::new_with_default_client(
"db-123".to_string(),
"users".to_string(),
"account-456".to_string(),
"token-789".to_string(),
key_fields,
value_fields,
metrics,
)
.expect("Failed to create context");
let url = context.api_url();
assert_eq!(
url,
"https://api.cloudflare.com/client/v4/accounts/account-456/d1/database/db-123/query"
);
}
#[test]
fn test_empty_field_values() {
let empty_values = FieldValues { fields: vec![] };
let json = value_to_json(&Value::Struct(empty_values)).expect("Failed to convert empty struct");
assert_eq!(json, serde_json::json!([]));
}
#[test]
fn test_deeply_nested_struct() {
let nested = Value::Struct(FieldValues {
fields: vec![Value::Struct(FieldValues {
fields: vec![Value::Basic(BasicValue::Str("deeply nested".into()))],
})],
});
let json = value_to_json(&nested).expect("Failed to convert nested struct");
assert_eq!(json, serde_json::json!([["deeply nested"]]));
}
#[test]
fn test_unicode_string_handling() {
let unicode_str = "Hello δΈη π Ω
Ψ±ΨΨ¨Ψ§";
let value = BasicValue::Str(unicode_str.into());
let json = basic_value_to_json(&value).expect("Failed to convert unicode string");
assert_eq!(json, serde_json::json!(unicode_str));
}
#[test]
fn test_empty_table_name() {
let table_id = D1TableId {
database_id: "db".to_string(),
table_name: "".to_string(),
};
let factory = D1TargetFactory;
let description = factory
.describe_resource(&table_id)
.expect("Failed to describe");
assert_eq!(description, "D1 table: db.");
}
#[test]
fn test_minimal_coverage_summary() {
println!("\n=== D1 Target Minimal Test Coverage Summary ===\n");
println!("β
Value Conversion Functions (API-compatible):");
println!(" - key_part_to_json: Str, Bool, Int64 tested");
println!(" - basic_value_to_json: Bool, Int64, Float32, Float64, Str tested");
println!(" - value_to_json: Null, Basic, Struct tested");
println!("\nβ
SQL Generation (No recoco dependencies):");
println!(" - value_type_to_sql: 5 types tested");
println!(" - create_table_sql: 2 scenarios tested");
println!(" - create_indexes_sql: 2 scenarios tested");
println!("\nβ
Setup State Management:");
println!(" - D1SetupState::new: tested");
println!(" - D1SetupChange methods: 3 types tested");
println!("\nβ
TargetFactoryBase Implementation:");
println!(" - name(): tested");
println!(" - describe_resource(): tested");
println!("\nβ
D1ExportContext:");
println!(" - Constructor validation: tested");
println!(" - API URL generation: tested");
println!("\nβ οΈ Not Covered (requires recoco API update):");
println!(" - Build operation with TypedExportDataCollectionSpec");
println!(" - diff_setup_states with CombinedState");
println!(" - check_state_compatibility tests");
println!(" - build_upsert_stmt / build_delete_stmt (need recoco types)");
println!(" - Complex value conversions (Bytes, Range, KTable with new types)");
println!("\nπ Estimated Coverage: 35-40% (API-compatible subset)");
println!(" - Pure functions: ~70% coverage");
println!(" - SQL generation: ~80% coverage");
println!(" - recoco-dependent: <10% coverage");
println!("\nπ‘ To achieve 80%+ coverage:");
println!(" - Update tests to match recoco API (Bytes, Arc, BTreeMap types)");
println!(" - Complete build/mutation tests with proper type construction");
println!(" - Add integration tests with mock D1 API\n");
}