#![cfg(feature = "sqlite")]
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use bashkit::Bash;
fn sqlite3_available() -> bool {
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
Command::new("sqlite3")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
})
}
macro_rules! require_sqlite3 {
() => {
if !sqlite3_available() {
eprintln!("skip: sqlite3 not on PATH");
return;
}
};
}
fn run_real_sqlite3(flags: &[&str], sql: &str) -> String {
let mut child = Command::new("sqlite3")
.args(flags)
.arg(":memory:")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn sqlite3");
child
.stdin
.as_mut()
.unwrap()
.write_all(sql.as_bytes())
.expect("feed sqlite3 stdin");
let out = child.wait_with_output().expect("wait sqlite3");
String::from_utf8(out.stdout).expect("sqlite3 stdout utf8")
}
async fn run_bashkit_sqlite(flags: &[&str], sql: &str) -> String {
let mut bash = Bash::builder()
.sqlite()
.env("BASHKIT_ALLOW_INPROCESS_SQLITE", "1")
.build();
let quoted_flags: Vec<String> = flags
.iter()
.map(|f| {
let escaped = f.replace('\'', "'\\''");
format!("'{escaped}'")
})
.collect();
let cmd = format!(
"sqlite {} :memory: <<'__BASHKIT_EOF__'\n{sql}\n__BASHKIT_EOF__",
quoted_flags.join(" "),
);
let r = bash.exec(&cmd).await.expect("bashkit exec");
assert_eq!(
r.exit_code, 0,
"bashkit sqlite failed: stderr={:?}",
r.stderr
);
r.stdout
}
async fn assert_matches(flags: &[&str], sql: &str) {
require_sqlite3!();
let host = run_real_sqlite3(flags, sql);
let bk = run_bashkit_sqlite(flags, sql).await;
pretty_assertions::assert_eq!(
bk,
host,
"bashkit and host sqlite3 disagree\nflags = {:?}\nsql = {sql:?}",
flags
);
}
#[tokio::test]
async fn list_mode_default_separator() {
assert_matches(&[], "SELECT 1, 'two', 3.5;").await;
}
#[tokio::test]
async fn list_mode_with_header() {
assert_matches(
&["-header"],
"CREATE TABLE t(x, y); \
INSERT INTO t VALUES (1, 'a'), (2, 'b'); \
SELECT * FROM t ORDER BY x;",
)
.await;
}
#[tokio::test]
async fn csv_mode_rfc_4180_quoting() {
assert_matches(&["-csv"], "SELECT 'she said \"hi\"' AS msg, 'a,b' AS list;").await;
}
#[tokio::test]
async fn csv_mode_with_header() {
assert_matches(
&["-csv", "-header"],
"CREATE TABLE t(name TEXT, age INTEGER); \
INSERT INTO t VALUES ('alice', 30), ('bob, jr.', 25); \
SELECT * FROM t ORDER BY name;",
)
.await;
}
#[tokio::test]
async fn tabs_mode_simple_select() {
assert_matches(
&["-tabs"],
"CREATE TABLE t(x, y); INSERT INTO t VALUES (1, 'a'); SELECT * FROM t;",
)
.await;
}
#[tokio::test]
async fn separator_flag_overrides_default() {
assert_matches(
&["-separator", ";"],
"CREATE TABLE t(x, y); INSERT INTO t VALUES (1, 2); SELECT * FROM t;",
)
.await;
}
#[tokio::test]
async fn nullvalue_renders_placeholder() {
assert_matches(&["-nullvalue", "(null)"], "SELECT NULL;").await;
}
#[tokio::test]
async fn aggregate_functions() {
assert_matches(
&[],
"CREATE TABLE t(x INTEGER); \
INSERT INTO t VALUES (1), (2), (3); \
SELECT COUNT(*), SUM(x), AVG(x), MIN(x), MAX(x) FROM t;",
)
.await;
}
#[tokio::test]
async fn order_by_limit_offset() {
assert_matches(
&[],
"CREATE TABLE t(x INTEGER); \
INSERT INTO t VALUES (5), (1), (3), (4), (2); \
SELECT x FROM t ORDER BY x LIMIT 2 OFFSET 1;",
)
.await;
}
#[tokio::test]
async fn pragma_user_version_round_trip() {
assert_matches(&[], "PRAGMA user_version = 42; PRAGMA user_version;").await;
}
#[tokio::test]
async fn string_ordering() {
assert_matches(
&[],
"CREATE TABLE t(s TEXT); \
INSERT INTO t VALUES ('zebra'), ('alpha'), ('mango'); \
SELECT s FROM t ORDER BY s;",
)
.await;
}
#[tokio::test]
async fn empty_result_set_is_empty() {
assert_matches(&[], "CREATE TABLE t(x); SELECT * FROM t WHERE 0 = 1;").await;
}
#[tokio::test]
async fn empty_result_set_with_header() {
assert_matches(&["-header"], "CREATE TABLE t(x, y); SELECT * FROM t;").await;
}
#[tokio::test]
async fn group_by_ordering() {
assert_matches(
&["-header"],
"CREATE TABLE t(g TEXT, v INTEGER); \
INSERT INTO t VALUES ('a', 1), ('a', 2), ('b', 5), ('b', 7); \
SELECT g, SUM(v) AS s FROM t GROUP BY g ORDER BY g;",
)
.await;
}
#[tokio::test]
async fn case_expression() {
assert_matches(
&[],
"CREATE TABLE t(x INTEGER); \
INSERT INTO t VALUES (1), (2), (3); \
SELECT x, CASE WHEN x > 1 THEN 'big' ELSE 'small' END FROM t ORDER BY x;",
)
.await;
}
#[tokio::test]
async fn coalesce_and_null_handling() {
assert_matches(
&[],
"CREATE TABLE t(a, b); \
INSERT INTO t VALUES (1, NULL), (NULL, 2), (3, 4); \
SELECT COALESCE(a, b, -1) FROM t ORDER BY rowid;",
)
.await;
}
#[tokio::test]
async fn subquery_in_where() {
assert_matches(
&[],
"CREATE TABLE t(x INTEGER); \
INSERT INTO t VALUES (1), (2), (3), (4); \
SELECT x FROM t WHERE x > (SELECT AVG(x) FROM t) ORDER BY x;",
)
.await;
}
#[tokio::test]
async fn join_inner() {
assert_matches(
&["-header"],
"CREATE TABLE a(id INTEGER, name TEXT); \
CREATE TABLE b(a_id INTEGER, val TEXT); \
INSERT INTO a VALUES (1, 'one'), (2, 'two'); \
INSERT INTO b VALUES (1, 'x'), (1, 'y'), (2, 'z'); \
SELECT a.name, b.val FROM a JOIN b ON a.id = b.a_id ORDER BY a.id, b.val;",
)
.await;
}
#[tokio::test]
async fn recursive_cte_unsupported_in_turso() {
require_sqlite3!();
let sql = "WITH RECURSIVE r(n) AS ( \
SELECT 1 UNION ALL SELECT n + 1 FROM r WHERE n < 5 \
) SELECT n FROM r;";
let host = run_real_sqlite3(&[], sql);
assert_eq!(host.trim(), "1\n2\n3\n4\n5".trim_end());
let mut bash = Bash::builder()
.sqlite()
.env("BASHKIT_ALLOW_INPROCESS_SQLITE", "1")
.build();
let cmd = format!("sqlite :memory: <<'__BASHKIT_EOF__'\n{sql}\n__BASHKIT_EOF__");
let r = bash.exec(&cmd).await.unwrap();
assert_ne!(
r.exit_code, 0,
"turso unexpectedly accepted a recursive CTE — rotate this test \
into `assert_matches` and remove the divergence note in the spec"
);
assert!(
r.stderr.contains("Recursive CTE") || r.stderr.to_lowercase().contains("recursive"),
"unexpected divergence reason: {:?}",
r.stderr,
);
}