mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-12-23 08:21:09 +00:00
prompt to opus 4.5: Create a parser that generates a Rust integration test a lá test_transactions.rs from files like .bugbase/17016242926153013649/plan.sql the "-- <integer>" means connection number, so as many connections should be created as there are distinct suffixes like that at the end of a line. lines BEGINNING with -- eg "-- begin testing 'ReadYourUpdatesBack'" can be ignored. the end result should be that the parser tool prints out a valid rust file containing the necessary imports and a test function containing the statements (modeled after test_transactions.rs tests). this should be a lightweight script or CLI tool in the simulator directory where you can give it a sql path and it will produce that test.
198 lines
6.4 KiB
Rust
Executable file
198 lines
6.4 KiB
Rust
Executable file
//! SQL Plan to Rust Integration Test Generator
|
|
//!
|
|
//! Usage: plan_to_test <path-to-plan.sql> > test_name.rs
|
|
//!
|
|
//! Parses SQL plan files from the simulator and generates a Rust integration test file.
|
|
//! Lines ending with "-- <number>" indicate which connection should execute that statement.
|
|
//! Lines beginning with "--" are comments and are ignored (except FAULT commands).
|
|
|
|
use regex::Regex;
|
|
use std::collections::BTreeSet;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().collect();
|
|
if args.len() != 2 {
|
|
eprintln!(
|
|
"Usage: {} <path-to-plan.sql> > output_test_name.rs",
|
|
args[0]
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let sql_path = &args[1];
|
|
let content = fs::read_to_string(sql_path).unwrap_or_else(|e| {
|
|
eprintln!("Failed to read SQL file: {e}");
|
|
std::process::exit(1);
|
|
});
|
|
|
|
let test_code = generate_test(&content, sql_path);
|
|
println!("{test_code}");
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SqlStatement {
|
|
sql: String,
|
|
connection: u32,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum PlanAction {
|
|
Sql(SqlStatement),
|
|
ReopenDatabase,
|
|
Disconnect { connection: u32 },
|
|
}
|
|
|
|
fn parse_plan(content: &str) -> (Vec<PlanAction>, BTreeSet<u32>) {
|
|
let mut actions = Vec::new();
|
|
let mut connections = BTreeSet::new();
|
|
|
|
// Regex to match "-- <number>" at end of line
|
|
let conn_suffix_re = Regex::new(r";\s*--\s*(\d+)\s*$").unwrap();
|
|
// Regex to match FAULT lines
|
|
let fault_re = Regex::new(r"^--\s*FAULT\s+'([^']+)';\s*--\s*(\d+)").unwrap();
|
|
|
|
for line in content.lines() {
|
|
let trimmed = line.trim();
|
|
|
|
// Skip empty lines
|
|
if trimmed.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Check for FAULT commands first
|
|
if let Some(caps) = fault_re.captures(trimmed) {
|
|
let fault_type = caps.get(1).unwrap().as_str();
|
|
let conn_num: u32 = caps.get(2).unwrap().as_str().parse().unwrap();
|
|
connections.insert(conn_num);
|
|
|
|
match fault_type {
|
|
"REOPEN_DATABASE" => {
|
|
actions.push(PlanAction::ReopenDatabase);
|
|
}
|
|
"DISCONNECT" => {
|
|
actions.push(PlanAction::Disconnect {
|
|
connection: conn_num,
|
|
});
|
|
}
|
|
_ => {
|
|
// Unknown fault type, skip
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Skip lines that begin with "--" (pure comments)
|
|
if trimmed.starts_with("--") {
|
|
continue;
|
|
}
|
|
|
|
// Try to find connection suffix
|
|
if let Some(caps) = conn_suffix_re.captures(trimmed) {
|
|
let conn_num: u32 = caps.get(1).unwrap().as_str().parse().unwrap();
|
|
connections.insert(conn_num);
|
|
|
|
// Extract SQL without the comment suffix
|
|
let sql_end = caps.get(0).unwrap().start();
|
|
let sql = trimmed[..sql_end + 1].trim().to_string(); // +1 to include the semicolon
|
|
|
|
// Skip lines marked as FAULTY QUERY
|
|
if trimmed.contains("FAULTY QUERY") {
|
|
continue;
|
|
}
|
|
|
|
actions.push(PlanAction::Sql(SqlStatement {
|
|
sql,
|
|
connection: conn_num,
|
|
}));
|
|
}
|
|
}
|
|
|
|
(actions, connections)
|
|
}
|
|
|
|
fn escape_sql_string(s: &str) -> String {
|
|
s.replace('\\', "\\\\")
|
|
.replace('"', "\\\"")
|
|
.replace('\n', "\\n")
|
|
}
|
|
|
|
fn generate_test(content: &str, sql_path: &str) -> String {
|
|
let (actions, connections) = parse_plan(content);
|
|
|
|
let path = Path::new(sql_path);
|
|
let test_name = path
|
|
.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("generated_test");
|
|
|
|
// Derive a test name from the path if it contains bugbase ID
|
|
let test_fn_name = if sql_path.contains(".bugbase/") {
|
|
let parts: Vec<&str> = sql_path.split('/').collect();
|
|
if let Some(pos) = parts.iter().position(|&p| p == ".bugbase") {
|
|
if pos + 1 < parts.len() {
|
|
format!("test_bugbase_{}", parts[pos + 1])
|
|
} else {
|
|
format!("test_{test_name}")
|
|
}
|
|
} else {
|
|
format!("test_{test_name}")
|
|
}
|
|
} else {
|
|
format!("test_{test_name}")
|
|
};
|
|
|
|
let num_connections = connections.iter().max().map(|&m| m + 1).unwrap_or(1);
|
|
|
|
let mut code = String::new();
|
|
|
|
// Imports
|
|
code.push_str("use std::sync::Arc;\n\n");
|
|
code.push_str("use turso_core::Connection;\n\n");
|
|
code.push_str("use crate::common::TempDatabase;\n\n");
|
|
|
|
// Test function
|
|
code.push_str("#[turso_macros::test]\n");
|
|
code.push_str(&format!("fn {test_fn_name}(tmp_db: TempDatabase) {{\n"));
|
|
|
|
// Create connections
|
|
code.push_str(" let mut connections: Vec<Arc<Connection>> = Vec::new();\n");
|
|
code.push_str(&format!(" for _ in 0..{num_connections} {{\n"));
|
|
code.push_str(" connections.push(tmp_db.connect_limbo());\n");
|
|
code.push_str(" }\n\n");
|
|
|
|
// Execute each statement
|
|
for action in &actions {
|
|
match action {
|
|
PlanAction::Sql(stmt) => {
|
|
let escaped_sql = escape_sql_string(&stmt.sql);
|
|
let connection = stmt.connection;
|
|
code.push_str(&format!(" // Connection {connection}\n"));
|
|
code.push_str(&format!(
|
|
" let _ = connections[{connection}].execute(\"{escaped_sql}\");\n\n"
|
|
));
|
|
}
|
|
PlanAction::ReopenDatabase => {
|
|
code.push_str(" // REOPEN_DATABASE - reopen database for all connections\n");
|
|
code.push_str(" drop(connections);\n");
|
|
code.push_str(" let tmp_db = TempDatabase::new_with_existent(&tmp_db.path);\n");
|
|
code.push_str(" let mut connections: Vec<Arc<Connection>> = Vec::new();\n");
|
|
code.push_str(&format!(" for _ in 0..{num_connections} {{\n"));
|
|
code.push_str(" connections.push(tmp_db.connect_limbo());\n");
|
|
code.push_str(" }\n\n");
|
|
}
|
|
PlanAction::Disconnect { connection } => {
|
|
code.push_str(&format!(" // DISCONNECT connection {connection}\n"));
|
|
code.push_str(&format!(
|
|
" drop(std::mem::replace(&mut connections[{connection}], tmp_db.connect_limbo()));\n\n"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
code.push_str("}\n");
|
|
|
|
code
|
|
}
|