mod type_stats;
mod utils;
use anyhow::Result;
use log::debug;
use rusqlite::Connection;
use crate::config::RuntimeConfig;
use crate::parsers::{FileType, ParseResult, column_stats};
use crate::results::{ColumnInfo, MiningResult, SqliteMetadata, TableInfo};
use memmap2::Mmap;
use type_stats::{compute_stats_for_type, fetch_column_as_strings};
fn handle_sqlite_error(
metadata: &mut SqliteMetadata,
file_path: &str,
error_msg: String,
) -> SqliteMetadata {
debug!("SQLite error for '{file_path}': {error_msg}");
metadata.error = Some(error_msg);
metadata.clone()
}
pub fn extract_sqlite_metadata(
content: &[u8],
stats: &ParseResult,
config: &RuntimeConfig,
) -> Result<SqliteMetadata> {
let (conn_opt, mut metadata) =
utils::open_sqlite_connection(content, stats.byte_count, &stats.file_path)?;
let Some((conn, _temp_file)) = conn_opt else {
return Ok(metadata);
};
metadata.page_size = utils::try_query_one(&conn, "PRAGMA page_size;", |r| r.get::<_, i64>(0))
.and_then(|v| usize::try_from(v).ok());
metadata.encoding = utils::try_query_one(&conn, "PRAGMA encoding;", |r| r.get::<_, String>(0));
metadata.sqlite_version =
utils::try_query_one(&conn, "SELECT sqlite_version();", |r| r.get::<_, String>(0));
let mut tables = Vec::new();
let mut total_rows = 0usize;
let table_names: Vec<String> = match utils::get_table_names(&conn) {
Ok(names) => names,
Err(error_msg) => {
return Ok(handle_sqlite_error(
&mut metadata,
&stats.file_path,
error_msg,
));
}
};
metadata.table_count = Some(table_names.len());
for table_name in &table_names {
let quoted_table = utils::quote_sql_identifier(table_name);
let (columns, primary_keys) = match utils::get_table_columns(&conn, table_name) {
Ok(x) => x,
Err(msg) => return Ok(handle_sqlite_error(&mut metadata, &stats.file_path, msg)),
};
let mut table_info = TableInfo {
name: table_name.clone(),
column_count: Some(columns.len()),
columns: if columns.is_empty() {
None
} else {
Some(columns)
},
primary_keys: if primary_keys.is_empty() {
None
} else {
Some(primary_keys)
},
..Default::default()
};
if let Some(n) = utils::get_table_row_count(&conn, "ed_table) {
table_info.row_count = Some(n);
total_rows += n;
}
let (foreign_keys, fk_columns) =
match utils::get_foreign_keys_for_table(&conn, "ed_table) {
Ok(x) => x,
Err(msg) => return Ok(handle_sqlite_error(&mut metadata, &stats.file_path, msg)),
};
if !foreign_keys.is_empty() {
table_info.foreign_keys = Some(foreign_keys);
}
if let Some(ref mut cols) = table_info.columns {
for col in cols.iter_mut() {
if fk_columns.contains(&col.name) {
col.is_foreign_key = Some(true);
}
}
}
ensure_column_stats(
&conn,
table_name,
table_info.columns.as_deref_mut(),
table_info.row_count.unwrap_or(0),
config,
);
let indexes = match utils::get_indexes_for_table(&conn, table_name, "ed_table) {
Ok(ix) => ix,
Err(msg) => return Ok(handle_sqlite_error(&mut metadata, &stats.file_path, msg)),
};
if !indexes.is_empty() {
table_info.indexes = Some(indexes);
}
tables.push(table_info);
}
metadata.total_rows = Some(total_rows);
if !tables.is_empty() {
metadata.tables = Some(tables);
}
Ok(metadata)
}
fn ensure_column_stats(
conn: &Connection,
table_name: &str,
columns: Option<&mut [ColumnInfo]>,
row_count: usize,
config: &RuntimeConfig,
) {
let Some(cols) = columns else { return };
if row_count == 0 {
for col in cols.iter_mut() {
utils::set_no_data_column_stats(col);
}
} else {
compute_column_statistics(conn, table_name, cols, config);
}
}
fn compute_column_statistics(
conn: &Connection,
table_name: &str,
columns: &mut [ColumnInfo],
config: &RuntimeConfig,
) {
let quoted_table = utils::quote_sql_identifier(table_name);
for col in columns.iter_mut() {
let quoted_col = utils::quote_sql_identifier(&col.name);
let values =
fetch_column_as_strings(conn, "ed_table, "ed_col, table_name, &col.name);
if values.is_empty() {
utils::set_no_data_column_stats(col);
continue;
}
let (null_pct, unique_count) = column_stats::compute_null_and_unique_stats(&values, config);
col.null_percentage = Some(null_pct);
col.unique_count = Some(unique_count);
compute_stats_for_type(conn, "ed_table, "ed_col, col, &values, config);
}
}
crate::no_template_mining!(
extract_sqlite_templates,
"`SQLite` files don't need template mining - return empty result. Only metadata is extracted (schema, tables, columns, etc.)."
);
pub fn process(
stats: &mut ParseResult,
mmap: &Mmap,
config: &RuntimeConfig,
) -> Result<MiningResult> {
crate::process_with_metadata!(
stats,
mmap,
config,
sqlite_metadata,
extract_sqlite_metadata(mmap, stats, config),
crate::results::SqliteMetadata,
FileType::Sqlite,
extract_sqlite_templates(mmap, stats, config)
)
}