sqlitepipe 0.1.3

A simple tool for piping the output of a command into sqlite databases.
Documentation
//! Internal utility for generating SQL statement strings.

use crate::{
    column::{Column, ColumnType},
    sanitizing::SanitizedIdentifier,
};

/// The size of the buffer (in bytes) used when reading and writing BLOB data.
pub const BLOB_BUF_SIZE: usize = 4096;

/// Generates a `DROP TABLE IF EXISTS` statement.
pub fn drop_table_if_exists(table_name: &SanitizedIdentifier) -> String {
    format!("DROP TABLE IF EXISTS {table_name};")
}

/// Generates a `CREATE TABLE IF NOT EXISTS` statement with the provided columns.
pub fn create_table_if_not_exists(
    table_name: &SanitizedIdentifier,
    columns: &[&SanitizedIdentifier],
) -> String {
    let column_part = columns
        .iter()
        .map(|v| v.as_str())
        .collect::<Vec<_>>()
        .join(", ");

    format!("CREATE TABLE IF NOT EXISTS {table_name} ({column_part})")
}

/// Returns the SQL string to query table metadata.
pub const fn select_pragram_table_info() -> &'static str {
    "SELECT name FROM pragma_table_info(?)"
}

/// Generates an `ALTER TABLE ADD COLUMN` statement.
pub fn alter_table_add_column(
    table_name: &SanitizedIdentifier,
    column: &SanitizedIdentifier,
) -> String {
    format!("ALTER TABLE {table_name} ADD COLUMN {column}")
}

/// Generates a parameterized `INSERT INTO` statement for standard rows.
pub fn insert_row(table_name: &SanitizedIdentifier, columns: &[Column]) -> String {
    let insert_values = columns.iter().map(|_| "?".to_string()).collect::<Vec<_>>();

    let column_names = columns
        .iter()
        .map(|v| v.sanitized_name())
        .collect::<Vec<_>>();

    insert_statement(table_name, &column_names, &insert_values)
}

/// Generates an `INSERT` statement designed to handle streaming BLOB data via aggregation.
pub fn insert_blob(
    table_name: &SanitizedIdentifier,
    columns: &[Column],
    blob_size: usize,
) -> String {
    let insert_values = columns
        .iter()
        .map(|v| {
            if v.column_type() == ColumnType::Blob {
                group_concat_blob(blob_size)
            } else {
                "?".to_string()
            }
        })
        .collect::<Vec<_>>();

    let column_names = columns
        .iter()
        .map(|v| v.sanitized_name())
        .collect::<Vec<_>>();

    insert_statement(table_name, &column_names, &insert_values)
}

/// Constructs a full `INSERT` statement from table, column names, and values.
fn insert_statement(
    table_name: &SanitizedIdentifier,
    column_names: &[&SanitizedIdentifier],
    insert_values: &[String],
) -> String {
    let insert_values = insert_values.join(", ");

    let column_part = column_names
        .iter()
        .map(|v| v.as_str())
        .collect::<Vec<_>>()
        .join(", ");

    format!("INSERT INTO {table_name} ({column_part}) VALUES ({insert_values});")
}

/// Returns a subquery that concatenates buffered BLOB segments from the temporary table.
pub fn group_concat_blob(blob_size: usize) -> String {
    if blob_size <= BLOB_BUF_SIZE {
        format!("SUBSTRING((SELECT CAST(data AS BLOB) FROM temp.blob_insert), 1, {blob_size})")
    } else {
        format!(
            "SUBSTRING((SELECT CAST(group_concat(data, '') AS BLOB) FROM temp.blob_insert ORDER BY rowid), 1, {blob_size})"
        )
    }
}

/// SQL for creating the temporary table used to buffer BLOB chunks.
pub const fn create_temporary_blob_table() -> &'static str {
    "CREATE TEMPORARY TABLE blob_insert(data);"
}

/// Generates an `INSERT` statement for a chunk of binary data using `ZEROBLOB`.
pub fn insert_zero_blob(buf_size: usize) -> String {
    format!("INSERT INTO temp.blob_insert(data) VALUES(ZEROBLOB({buf_size}));")
}