sqlite-collections 0.1.0

Rust collection types backed by sqlite database files.
Documentation
use std::time::{SystemTime, UNIX_EPOCH};

use crate::identifier::Identifier;
use crate::APPLICATION_ID;

mod error;
pub use error::{SetVersionError, SetupError};
use rusqlite::{
    functions::{Context, FunctionFlags},
    params, Connection,
};

mod savepointable;
pub use savepointable::Savepointable;

mod as_connection;
pub use as_connection::AsConnection;

fn unixepoch(_: &Context) -> rusqlite::Result<i64> {
    let now = SystemTime::now();
    let stamp = now.duration_since(UNIX_EPOCH).map_or_else(
        move |_| -(UNIX_EPOCH.duration_since(now).unwrap().as_secs() as i64),
        move |dur| dur.as_secs() as i64,
    );
    Ok(stamp)
}

/// Get the closest trailer to STRICT, WITHOUT ROWID for this version of sqlite.
pub fn strict_without_rowid() -> &'static str {
    match rusqlite::version_number() {
        i32::MIN..=3008001 => "",
        3008002..=3036999 => " WITHOUT ROWID",
        3037000..=i32::MAX => " STRICT, WITHOUT ROWID",
    }
}

/// Get the closest trailer to STRICT for this version of sqlite.
pub fn strict() -> &'static str {
    match rusqlite::version_number() {
        i32::MIN..=3036999 => "",
        3037000..=i32::MAX => " STRICT",
    }
}

/// Get the ANY value based on the SQLite version.
pub fn any() -> &'static str {
    if rusqlite::version_number() < 3037000 {
        "BLOB"
    } else {
        "ANY"
    }
}

pub fn has_upsert() -> bool {
    rusqlite::version_number() >= 3024000
}

/// Create the collecions meta table, set and check the application_id and
/// user_version, make the unixepoch function available.
pub fn setup(
    connection: &Connection,
    database: &Identifier,
    table: &Identifier,
    r#type: &str,
) -> Result<i64, SetupError> {
    let application_id: i32 =
        connection.query_row(&format!("PRAGMA {database}.application_id"), [], |row| {
            row.get(0)
        })?;

    match application_id {
        0 => {
            connection.execute(
                &format!("PRAGMA {database}.application_id = {APPLICATION_ID}"),
                [],
            )?;
        }
        crate::APPLICATION_ID => (),
        id => {
            return Err(SetupError::ApplicationId(id));
        }
    }

    let mut user_version: i32 =
        connection.query_row(&format!("PRAGMA {database}.user_version"), [], |row| {
            row.get(0)
        })?;

    if user_version < 0 {
        return Err(SetupError::UserVersion(user_version));
    }

    let prev_user_version = user_version;

    if user_version < 1 {
        let trailer = strict_without_rowid();

        // create collections meta table
        connection.execute(
            &format!(
                "CREATE TABLE {database}.collections (
                    name TEXT PRIMARY KEY NOT NULL,
                    type TEXT NOT NULL,
                    version INTEGER NOT NULL
                ){trailer}"
            ),
            [],
        )?;

        user_version = 1;
    }
    if user_version > 1 {
        return Err(SetupError::UserVersion(user_version));
    }

    if user_version != prev_user_version {
        connection.execute(
            &format!("PRAGMA {database}.user_version = {user_version}"),
            [],
        )?;
    }

    // Create unixepoch on the connection if the version needs it.
    if rusqlite::version_number() < 3038000 {
        connection.create_scalar_function(
            "unixepoch",
            0,
            FunctionFlags::SQLITE_INNOCUOUS,
            unixepoch,
        )?;
    }
    {
        let mut statement = connection.prepare_cached(&format!(
            "SELECT type, version FROM {database}.collections WHERE name=?"
        ))?;
        let table_str = &**table;
        let version: i64 = match statement.query_row(params![table_str], |row| {
            let r#type: String = row.get(0)?;
            Ok((r#type, row.get(1)?))
        }) {
            Ok((prev_type, version)) => {
                if r#type != prev_type {
                    return Err(SetupError::TableType {
                        expected: r#type.into(),
                        actual: prev_type,
                    });
                }
                version
            }
            Err(rusqlite::Error::QueryReturnedNoRows) => {
                let mut statement = connection.prepare_cached(&format!(
                    "INSERT INTO {database}.collections (name, type, version) VALUES (?, ?, ?)"
                ))?;
                statement.execute(params![table_str, r#type, 0i64])?;
                0
            }
            Err(e) => return Err(e.into()),
        };
        Ok(version)
    }
}

/// Set the individual table's version.
pub fn set_version(
    connection: &Connection,
    database: &Identifier,
    table: &Identifier,
    version: i64,
) -> Result<(), SetVersionError> {
    let table_str = &**table;
    let mut statement = connection.prepare_cached(&format!(
        "UPDATE {database}.collections SET version=? WHERE name=?"
    ))?;
    statement.execute(params![version, table_str])?;
    Ok(())
}