sqlite3_ext 0.2.1

Build loadable extensions for SQLite using Rust
Documentation
use super::*;
use indoc::indoc;
use lazy_static::lazy_static;
use pretty_assertions::assert_eq;
use regex::Regex;
use std::str::from_utf8;

fn setup() -> Result<(Database, Rc<RefCell<Vec<u8>>>)> {
    let conn = Database::open(":memory:")?;
    let out = Rc::new(RefCell::new(vec![]));
    init(&conn, out.clone())?;
    conn.execute(
        "CREATE VIRTUAL TABLE temp.log USING vtablog(schema='CREATE TABLE x(a,b,c)', rows=3)",
        (),
    )?;
    Ok((conn, out))
}

#[cfg(modern_sqlite)]
lazy_static! {
    static ref IGNORED_LINES: Regex = Regex::new("(?m)^<M.*?\n").unwrap();
    static ref INCLUDED_LINES: Regex = Regex::new("(?m)^=M (.*?\n)").unwrap();
}
#[cfg(not(modern_sqlite))]
lazy_static! {
    static ref IGNORED_LINES: Regex = Regex::new("(?m)^=M.*?\n").unwrap();
    static ref INCLUDED_LINES: Regex = Regex::new("(?m)^<M (.*?\n)").unwrap();
}

fn patch_output(input: String) -> String {
    let input = IGNORED_LINES.replace_all(&input, "");
    INCLUDED_LINES.replace_all(&input, "$1").to_string()
}

#[test]
fn read() -> Result<()> {
    let (conn, out) = setup()?;
    let ret: Vec<Vec<String>> = conn
        .prepare("SELECT * FROM log WHERE a IN ('a1', 'a2')")?
        .query(())?
        .map(|row| {
            Ok(vec![
                row[0].get_str()?.to_owned(),
                row[1].get_str()?.to_owned(),
                row[2].get_str()?.to_owned(),
            ])
        })
        .collect()?;
    drop(conn);
    assert_eq!(
        ret,
        (1..3)
            .map(|i| vec![format!("a{}", i), format!("b{}", i), format!("c{}", i)])
            .collect::<Vec<Vec<String>>>()
    );
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = patch_output(indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        <M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: true, argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98 })
        <M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: false, argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98 })
        =M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: true, rhs: Err(Sqlite(12, "unknown operation")), collation: Ok("BINARY"), argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98, estimated_rows: 25, scan_flags: 0, columns_used: 7 })
        =M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: false, rhs: Err(Sqlite(12, "unknown operation")), collation: Ok("BINARY"), argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98, estimated_rows: 25, scan_flags: 0, columns_used: 7 })
        open(tab=100, cursor=101)
        filter(tab=100, cursor=101, args=[Text(Ok("a1"))])
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a0")
        next(tab=100, cursor=101)
          rowid 0 -> 1
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a1")
        column(tab=100, cursor=101, idx=0) -> Ok("a1")
        column(tab=100, cursor=101, idx=1) -> Ok("b1")
        column(tab=100, cursor=101, idx=2) -> Ok("c1")
        next(tab=100, cursor=101)
          rowid 1 -> 2
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a2")
        next(tab=100, cursor=101)
          rowid 2 -> 3
        eof(tab=100, cursor=101) -> true
        filter(tab=100, cursor=101, args=[Text(Ok("a2"))])
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a0")
        next(tab=100, cursor=101)
          rowid 0 -> 1
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a1")
        next(tab=100, cursor=101)
          rowid 1 -> 2
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a2")
        column(tab=100, cursor=101, idx=0) -> Ok("a2")
        column(tab=100, cursor=101, idx=1) -> Ok("b2")
        column(tab=100, cursor=101, idx=2) -> Ok("c2")
        next(tab=100, cursor=101)
          rowid 2 -> 3
        eof(tab=100, cursor=101) -> true
        drop(tab=100, cursor=101)
        disconnect(tab=100)
        drop(tab=100)
    "#}.to_owned());
    assert_eq!(out, expected);
    Ok(())
}

#[test]
fn insert() -> Result<()> {
    let (conn, out) = setup()?;
    conn.execute("INSERT INTO log VALUES ( 1, 2, 3 ), (4, 5, 6)", ())?;
    drop(conn);
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        begin(tab=100, transaction=102)
        update(tab=100, args=ChangeInfo { change_type: Insert, rowid: Null, args: [Null, Integer(1), Integer(2), Integer(3)], conflict_mode: Abort })
        update(tab=100, args=ChangeInfo { change_type: Insert, rowid: Null, args: [Null, Integer(4), Integer(5), Integer(6)], conflict_mode: Abort })
        sync(tab=100, transaction=102)
        commit(tab=100, transaction=102)
        drop_transaction(tab=100, transaction=102)
        disconnect(tab=100)
        drop(tab=100)
    "#};
    assert_eq!(out, expected);
    Ok(())
}

#[test]
fn update() -> Result<()> {
    let (conn, out) = setup()?;
    conn.execute("UPDATE log SET a = b WHERE rowid = 1", ())?;
    drop(conn);
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = patch_output(indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        <M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: -1, op: Eq, usable: true, argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98 })
        =M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: -1, op: Eq, usable: true, rhs: Ok(Integer(1)), collation: Ok("BINARY"), argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98, estimated_rows: 25, scan_flags: 0, columns_used: 18446744073709551615 })
        begin(tab=100, transaction=102)
        open(tab=100, cursor=101)
        filter(tab=100, cursor=101, args=[Integer(1)])
        eof(tab=100, cursor=101) -> false
        rowid(tab=100, cursor=101) -> 0
        next(tab=100, cursor=101)
          rowid 0 -> 1
        eof(tab=100, cursor=101) -> false
        rowid(tab=100, cursor=101) -> 1
        column(tab=100, cursor=101, idx=1) -> Ok("b1")
        <M column(tab=100, cursor=101, idx=1) -> Ok("b1")
        <M column(tab=100, cursor=101, idx=2) -> Ok("c1")
        =M column(tab=100, cursor=101, idx=1) -> Err(NoChange)
        =M column(tab=100, cursor=101, idx=2) -> Err(NoChange)
        rowid(tab=100, cursor=101) -> 1
        rowid(tab=100, cursor=101) -> 1
        next(tab=100, cursor=101)
          rowid 1 -> 2
        eof(tab=100, cursor=101) -> false
        rowid(tab=100, cursor=101) -> 2
        next(tab=100, cursor=101)
          rowid 2 -> 3
        eof(tab=100, cursor=101) -> true
        <M update(tab=100, args=ChangeInfo { change_type: Update, rowid: Integer(1), args: [Integer(1), Text(Ok("b1")), Text(Ok("b1")), Text(Ok("c1"))], conflict_mode: Abort })
        =M update(tab=100, args=ChangeInfo { change_type: Update, rowid: Integer(1), args: [Integer(1), Text(Ok("b1")), Null, Null], conflict_mode: Abort })
        =M   unchanged: [2, 3]
        drop(tab=100, cursor=101)
        sync(tab=100, transaction=102)
        commit(tab=100, transaction=102)
        drop_transaction(tab=100, transaction=102)
        disconnect(tab=100)
        drop(tab=100)
    "#}.to_owned());
    assert_eq!(out, expected);
    Ok(())
}

#[test]
fn delete() -> Result<()> {
    let (conn, out) = setup()?;
    conn.execute("DELETE FROM log WHERE a = 'a1'", ())?;
    drop(conn);
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = patch_output(indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        <M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: true, argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98 })
        =M best_index(tab=100, index_info=IndexInfo { constraints: [IndexInfoConstraint { column: 0, op: Eq, usable: true, rhs: Ok(Text(Ok("a1"))), collation: Ok("BINARY"), argv_index: None, omit: false }], order_by: [], index_num: 0, index_str: None, order_by_consumed: false, estimated_cost: 5e98, estimated_rows: 25, scan_flags: 0, columns_used: 1 })
        begin(tab=100, transaction=102)
        open(tab=100, cursor=101)
        filter(tab=100, cursor=101, args=[Text(Ok("a1"))])
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a0")
        next(tab=100, cursor=101)
          rowid 0 -> 1
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a1")
        rowid(tab=100, cursor=101) -> 1
        next(tab=100, cursor=101)
          rowid 1 -> 2
        eof(tab=100, cursor=101) -> false
        column(tab=100, cursor=101, idx=0) -> Ok("a2")
        next(tab=100, cursor=101)
          rowid 2 -> 3
        eof(tab=100, cursor=101) -> true
        update(tab=100, args=ChangeInfo { change_type: Delete, rowid: Integer(1), args: [], conflict_mode: Abort })
        drop(tab=100, cursor=101)
        sync(tab=100, transaction=102)
        commit(tab=100, transaction=102)
        drop_transaction(tab=100, transaction=102)
        disconnect(tab=100)
        drop(tab=100)
    "#}.to_owned());
    assert_eq!(out, expected);
    Ok(())
}

#[test]
fn rename() -> Result<()> {
    let (conn, out) = setup()?;
    conn.execute("ALTER TABLE log RENAME to newname", ())?;
    conn.execute("DROP TABLE newname", ())?;
    drop(conn);
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        rename(tab=100, name="newname")
        disconnect(tab=100)
        drop(tab=100)
        connect(tab=200, args=["vtablog", "temp", "newname", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        destroy(tab=200)
        drop(tab=200)
    "#};
    assert_eq!(out, expected);
    Ok(())
}

#[test]
#[cfg(modern_sqlite)]
fn shadow_name() -> Result<()> {
    let (conn, out) = setup()?;
    conn.db_config_defensive(true)?;
    match conn.execute("CREATE TABLE log_shadow (a, b, c)", ()) {
        Err(_) => (),
        _ => panic!("expected error, got ok"),
    }
    drop(conn);
    let out = from_utf8(&out.borrow()).unwrap().to_owned();
    let expected = indoc! {r#"
        create(tab=100, args=["vtablog", "temp", "log", "schema='CREATE TABLE x(a,b,c)'", "rows=3"])
        begin(tab=100, transaction=101)
        sync(tab=100, transaction=101)
        commit(tab=100, transaction=101)
        drop_transaction(tab=100, transaction=101)
        disconnect(tab=100)
        drop(tab=100)
    "#};
    assert_eq!(out, expected);
    Ok(())
}