testing/runner/docs/backends/cli.md
The CLI backend executes SQL by spawning the tursodb CLI tool as a subprocess.
┌─────────────────────────────────────────┐
│ Test Runner │
│ ┌───────────────────────────────────┐ │
│ │ CliBackend │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ CliDatabaseInstance │ │ │
│ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ tursodb subprocess │ │ │ │
│ │ │ │ stdin/stdout pipes │ │ │ │
│ │ │ └───────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
pub struct CliBackend {
/// Path to the tursodb binary
binary_path: PathBuf,
/// Working directory for the CLI
working_dir: Option<PathBuf>,
/// Timeout for query execution
timeout: Duration,
}
let backend = CliBackend::new("./target/debug/tursodb")
.with_timeout(Duration::from_secs(30));
let config = DatabaseConfig {
location: DatabaseLocation::Memory,
readonly: false,
};
let mut db = backend.create_database(&config).await?;
let result = db.execute("SELECT 1;").await?;
db.close().await?;
:memory: databases: Use the CLI with :memory: path:temp: databases: Create a temp file and use it as the database pathThe backend communicates with tursodb using:
-m list) for pipe-separated outputList mode produces output like:
column1|column2|column3
value1|value2|value3
The backend parses this into Vec<Vec<String>>.
Errors are detected by:
# What the backend does internally:
echo "SELECT 1, 'hello';" | tursodb :memory: -m list
# Output:
1|hello
with_timeout()BackendError::Timeout returned.tables, .schema) may not work as expectedexecute() call is a separate CLI invocationexecute() calls for :memory: databasesEach execute() call spawns a new tursodb process. This ensures:
For better performance with multiple queries, consider:
; separatorFor :temp: databases:
DatabaseInstance::close() is calledtempfile crate for safe cleanuppub struct CliBackend {
binary_path: PathBuf,
working_dir: Option<PathBuf>,
timeout: Duration, // Default: 30 seconds
}
pub struct CliDatabaseInstance {
binary_path: PathBuf,
working_dir: Option<PathBuf>,
db_path: String,
readonly: bool,
timeout: Duration,
_temp_file: Option<NamedTempFile>, // Keeps temp file alive
}
Setup Buffering for Memory Databases: For :memory: databases, execute_setup() buffers SQL instead of executing immediately. This is necessary because each CLI invocation creates a fresh in-memory database. The buffered SQL is combined with the test query in execute().
Temp File Lifetime: The _temp_file field keeps the NamedTempFile alive for the duration of the database instance. The underscore prefix indicates it's intentionally unused directly - its presence prevents the temp file from being deleted prematurely.
Stdin/Stdout Communication: SQL is written to stdin, then stdin is closed to signal end of input. Results are read from stdout after process completion. The -q flag suppresses the banner output.
Error Detection Strategy:
QueryResult::error() rather than BackendError to allow error expectation testsOutput Parsing: List mode (-m list) produces pipe-separated values which are split and collected into Vec<Vec<String>>.
let backend = CliBackend::new("./target/debug/tursodb")
.with_working_dir("/path/to/workdir")
.with_timeout(Duration::from_secs(60));
async fn execute(&mut self, sql: &str) -> Result<QueryResult, BackendError> {
// 1. Build command with args
let mut cmd = Command::new(&self.binary_path);
cmd.arg(&self.db_path).arg("-m").arg("list");
// 2. Set up pipes
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
// 3. Spawn and write SQL
let mut child = cmd.spawn()?;
child.stdin.as_mut().unwrap().write_all(sql.as_bytes()).await?;
child.stdin.take(); // Close stdin
// 4. Wait with timeout
let output = timeout(self.timeout, child.wait_with_output()).await??;
// 5. Parse and return
Ok(QueryResult::success(parse_list_output(&stdout)))
}
The module includes tests for output parsing:
test_parse_list_output_empty - Empty output returns empty vectest_parse_list_output_single_column - Single values per rowtest_parse_list_output_multiple_columns - Pipe-separated columnstest_parse_list_output_empty_values - Handles empty columns (1||3)test_parse_list_output_trailing_newline - Ignores trailing newlines