Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 58 additions & 16 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@ pub(crate) fn parse_args() -> Args {
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub(crate) struct Args {
#[command(flatten)]
pub(crate) verbose: Verbose,

#[command(flatten)]
pub(crate) pattern: PatternArgs,

#[command(flatten)]
pub(crate) query: QueryArgs,

#[arg(help = indoc!("
Database URI to connect to.

General URL format is <database>://username:password@hostname?<options>.

For SQLite username, password are never, hostname is just a file path.
If none of options provided, uri provided support direct filename.

Currently supported databases: SQLite 3.x with prefix sqlite
"
))]
pub(crate) database_uri: String,
}

#[derive(Parser, Debug)]
pub struct Verbose {
#[arg(help = "Decrease verbosity")]
#[arg(short = 'q', long = "quiet")]
#[arg(action=ArgAction::Count)]
Expand All @@ -17,7 +42,31 @@ pub(crate) struct Args {
#[arg(short = 'v', long = "verbose")]
#[arg(action=ArgAction::Count)]
pub(crate) verbose: u8,
}

#[derive(Parser, Debug)]
pub struct PatternArgs {
#[arg(short = 'F', long = "pattern-fixed")]
#[arg(help = "Pattern is a fixed string")]
#[arg(action=ArgAction::SetTrue)]
pub(crate) fixed: bool,

#[arg(short = 'W', long = "pattern-whole")]
#[arg(help = "Pattern matches whole string")]
#[arg(action=ArgAction::SetTrue)]
pub(crate) whole_string: bool,

#[arg(short = 'i', long = "pattern-case-insensitive")]
#[arg(help = "Pattern is case insensitive")]
#[arg(action=ArgAction::SetTrue)]
pub(crate) case_insensitive: bool,

#[arg(help = "Pattern to match every cell with")]
pub(crate) pattern: String,
}

#[derive(Parser, Debug)]
pub struct QueryArgs {
#[arg(short = 't', long = "table")]
#[arg(help = "Table or view to query. Can be used multiple times")]
#[arg(action=ArgAction::Append)]
Expand All @@ -28,24 +77,17 @@ pub(crate) struct Args {
#[arg(action=ArgAction::Append)]
pub(crate) query: Vec<String>,

#[arg(short = 'i', long = "ignore")]
#[arg(short = 'I', long = "ignore")]
#[arg(help = "Ignore non-readonly queries")]
#[arg(action=ArgAction::SetTrue)]
pub(crate) ignore_non_readonly: bool,
}

#[arg(help = "Pattern to match every cell with")]
pub(crate) pattern: String,

#[arg(help = indoc!("
Database URI to connect to.

General URL format is <database>://username:password@hostname?<options>.

For SQLite username, password are never, hostname is just a file path.
If none of options provided, uri provided support direct filename.

Currently supported databases: SQLite 3.x with prefix sqlite
"
))]
pub(crate) database_uri: String,
impl Verbose {
/// Verbosity level.
///
/// Default log level is Info (2)
pub fn level(&self) -> i16 {
2 + i16::from(self.verbose) - i16::from(self.quiet)
}
}
63 changes: 40 additions & 23 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::io::stdin;
use error::Level;
use error::SQLError;
use matching::sqlite_check_rows;
use pattern::{Pattern, PatternKind};
use pattern::Pattern;
use query::{prepare_queries, SelectVariant};
use sqlparser::dialect::SQLiteDialect;

Expand All @@ -21,42 +21,59 @@ use sqlx::{sqlite::SqliteConnectOptions, Executor as _, Pool, Row as _, Sqlite,
#[tokio::main()]
async fn main() {
let args = args::parse_args();
// Set default log level to 2.
let quiet_level: i16 = 2 + i16::from(args.verbose) - i16::from(args.quiet);

stderrlog::new()
.module(module_path!())
.quiet(quiet_level < 0)
.verbosity(quiet_level.unsigned_abs() as usize)
.timestamp(stderrlog::Timestamp::Off)
.color(stderrlog::ColorChoice::Auto)
.init()
.unwrap();
setup_logging(args.verbose.level());

let pattern = match Pattern::new(args.pattern.as_str(), &PatternKind::Regex) {
Ok(pattern) => pattern,
Err(err) => std::process::exit(err.report(Level::Error)),
};
let pattern = create_pattern(&args.pattern)
.unwrap_or_else(|error| std::process::exit(error.report(Level::Error)));

let queries = match read_queries(args.query) {
let queries = match read_queries(args.query.query) {
Ok(queries) => queries,
Err(err) => std::process::exit(err.report(Level::Error)),
Err(error) => std::process::exit(error.report(Level::Error)),
};

match process_sqlite_database(
args.database_uri,
pattern,
args.table,
args.query.table,
queries,
args.ignore_non_readonly,
args.query.ignore_non_readonly,
)
.await
{
Ok(()) => {}
Err(err) => std::process::exit(err.report(Level::Error)),
Err(error) => std::process::exit(error.report(Level::Error)),
}
}

fn setup_logging(verbosity_level: i16) {
stderrlog::new()
.module(module_path!())
.quiet(verbosity_level < 0)
.verbosity(verbosity_level.unsigned_abs() as usize)
.timestamp(stderrlog::Timestamp::Off)
.color(stderrlog::ColorChoice::Auto)
.init()
.unwrap();
}

fn create_pattern(options: &args::PatternArgs) -> Result<Pattern, SQLError> {
let kind = if options.fixed {
pattern::PatternKind::Fixed
} else {
pattern::PatternKind::Regex
};

Pattern::new(
options.pattern.as_str(),
&kind,
pattern::PatternOptions {
case_insensitive: options.case_insensitive,
whole_string: options.whole_string,
},
)
}

async fn process_sqlite_database(
database_uri: String,
pattern: Pattern,
Expand Down Expand Up @@ -114,14 +131,14 @@ async fn sqlite_select_tables(db: &Pool<Sqlite>) -> Result<Vec<String>, SQLError
let result = db
.fetch_all(select_query)
.await
.map_err(|err| SQLError::SqlX(("fetch tables".into(), err)))?;
.map_err(|error| SQLError::SqlX(("fetch tables".into(), error)))?;

Ok(result
.into_iter()
.filter_map(|row| match row.try_get::<String, &str>("name") {
Ok(value) => Some(value),
Err(err) => {
SQLError::SqlX(("fetch tables".into(), err)).report(Level::Warn);
Err(error) => {
SQLError::SqlX(("fetch tables".into(), error)).report(Level::Warn);
None
}
})
Expand Down
60 changes: 54 additions & 6 deletions src/pattern.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,73 @@
use crate::error::SQLError;

pub(crate) enum PatternKind {
Fixed,
Regex,
}
pub(crate) struct PatternOptions {
pub case_insensitive: bool,
pub whole_string: bool,
}

pub(crate) enum Pattern {
Regex(regex::Regex),
Always,
Fixed((String, PatternOptions)),
Regex((regex::Regex, PatternOptions)),
}

impl Pattern {
pub fn new(pattern: &str, kind: &PatternKind) -> Result<Self, SQLError> {
pub fn new(
pattern: &str,
kind: &PatternKind,
options: PatternOptions,
) -> Result<Self, SQLError> {
match kind {
PatternKind::Regex => regex::Regex::new(pattern)
.map(Self::Regex)
.map_err(SQLError::Regex),
PatternKind::Regex => {
if pattern.is_empty() || pattern == ".*" {
Ok(Self::Always)
} else {
regex::RegexBuilder::new(pattern)
.case_insensitive(options.case_insensitive)
.build()
.map(|value| Self::Regex((value, options)))
.map_err(SQLError::Regex)
}
}
PatternKind::Fixed => Ok(if pattern.is_empty() {
Self::Always
} else {
let pattern = if options.case_insensitive {
pattern.to_lowercase()
} else {
pattern.to_owned()
};
Self::Fixed((pattern, options))
}),
}
}

pub fn is_match(&self, value: &str) -> bool {
match self {
Pattern::Regex(regex) => regex.is_match(value),
Pattern::Always => true,
Pattern::Fixed((pattern, options)) => {
match (options.case_insensitive, options.whole_string) {
(true, true) => value.to_lowercase() == *pattern,
(false, true) => value == pattern,
(true, false) => value.to_lowercase().contains(pattern),
(false, false) => value.contains(pattern),
}
}
Pattern::Regex((pattern, options)) => {
let Some(pattern_match) = pattern.find(value) else {
return false;
};

if options.whole_string {
pattern_match.len() == value.len()
} else {
true
}
}
}
}
}