352 lines
9.9 KiB
Rust
352 lines
9.9 KiB
Rust
//! Defines the query struct and related types.
|
|
//! Query parsing is handled in the `parser` module
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use crate::expr::Expr;
|
|
use crate::field::Field;
|
|
use crate::query::TraversalMode::Bfs;
|
|
|
|
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)]
|
|
/// Represents a query to be executed on .
|
|
///
|
|
pub struct Query {
|
|
/// File fields to be selected
|
|
pub fields: Vec<Expr>,
|
|
/// Root directories to search
|
|
pub roots: Vec<Root>,
|
|
/// "where" filter expression
|
|
pub expr: Option<Expr>,
|
|
/// Fields to group by
|
|
pub grouping_fields: Vec<Expr>,
|
|
/// Fields to order by
|
|
pub ordering_fields: Vec<Expr>,
|
|
/// Ordering direction (true for asc, false for desc)
|
|
pub ordering_asc: Vec<bool>,
|
|
/// Max amount of results to return
|
|
pub limit: u32,
|
|
pub offset: u32,
|
|
/// Output format
|
|
pub output_format: OutputFormat,
|
|
pub raw_query: String,
|
|
}
|
|
|
|
impl Query {
|
|
pub fn get_all_fields(&self) -> HashSet<Field> {
|
|
let mut result = HashSet::new();
|
|
|
|
for column_expr in &self.fields {
|
|
result.extend(column_expr.get_required_fields());
|
|
}
|
|
|
|
// Ordering expressions may reference columns that are not selected
|
|
// (e.g. `select ext, count(*) ... order by max(modified)`); the
|
|
// aggregation scan must collect their per-file values too.
|
|
for ordering_expr in &self.ordering_fields {
|
|
result.extend(ordering_expr.get_required_fields());
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
pub fn is_ordered(&self) -> bool {
|
|
!self.ordering_fields.is_empty()
|
|
}
|
|
|
|
pub fn has_aggregate_column(&self) -> bool {
|
|
self.fields.iter().any(|f| f.has_aggregate_function())
|
|
}
|
|
|
|
/// True when results are produced per group rather than per file: either
|
|
/// an aggregate column is selected, or GROUP BY is present. Like in SQL,
|
|
/// `select ext from dir group by ext` collapses rows into distinct groups
|
|
/// even though no aggregate function appears in the SELECT list.
|
|
pub fn is_aggregated(&self) -> bool {
|
|
self.has_aggregate_column() || !self.grouping_fields.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)]
|
|
/// Represents a root directory to start the search from, with traversal options.
|
|
/// When `subquery` is present, the root represents the result set of an inner
|
|
/// query rather than a filesystem path; `path` is unused in that case.
|
|
pub struct Root {
|
|
pub path: String,
|
|
pub options: RootOptions,
|
|
pub subquery: Option<Box<Query>>,
|
|
}
|
|
|
|
macro_rules! root_options {
|
|
(
|
|
$(#[$struct_attrs:meta])*
|
|
$vis:vis struct $struct_name:ident {
|
|
$(
|
|
$(
|
|
@text = [$($text:literal),*], description = $description:literal
|
|
)+
|
|
$(#[$field_attrs:meta])*
|
|
$field_vis:vis $field:ident: $field_type:ty
|
|
),*
|
|
$(,)?
|
|
}
|
|
) => {
|
|
$(#[$struct_attrs])*
|
|
$vis struct $struct_name {
|
|
$(
|
|
$(#[$field_attrs])*
|
|
$field_vis $field: $field_type,
|
|
)*
|
|
}
|
|
|
|
impl $struct_name {
|
|
pub fn get_names_and_descriptions() -> Vec<(Vec<&'static str>, &'static str)> {
|
|
vec![
|
|
$(
|
|
$(#[$field_attrs])*
|
|
$(
|
|
(vec![$($text,)*], $description),
|
|
)+
|
|
)*
|
|
]
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
root_options! {
|
|
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)]
|
|
pub struct RootOptions {
|
|
@text = ["mindepth"], description = "Minimum depth to search"
|
|
pub min_depth: u32,
|
|
|
|
@text = ["maxdepth", "depth"], description = "Maximum depth to search"
|
|
pub max_depth: u32,
|
|
|
|
@text = ["archives", "arc"], description = "Whether to search archives"
|
|
pub archives: bool,
|
|
|
|
@text = ["symlinks", "sym"], description = "Whether to follow symlinks"
|
|
pub symlinks: bool,
|
|
|
|
@text = ["gitignore", "git"], description = "Search respects .gitignore files found"
|
|
@text = ["nogitignore", "nogit"], description = "Disable .gitignore parsing during the search"
|
|
pub gitignore: Option<bool>,
|
|
|
|
@text = ["hgignore", "hg"], description = "Search respects .hgignore files found"
|
|
@text = ["nohgignore", "nohg"], description = "Disable .hgignore parsing during the search"
|
|
pub hgignore: Option<bool>,
|
|
|
|
@text = ["dockerignore", "docker"], description = "Search respects .dockerignore files found"
|
|
@text = ["nodockerignore", "nodocker"], description = "Disable .dockerignore parsing during the search"
|
|
pub dockerignore: Option<bool>,
|
|
|
|
@text = ["dfs"], description = "Depth-first search mode"
|
|
@text = ["bfs"], description = "Breadth-first search mode (default)"
|
|
pub traversal: TraversalMode,
|
|
|
|
@text = ["regexp", "rx"], description = "Treat the path as a regular expression"
|
|
pub regexp: bool,
|
|
|
|
@text = ["as"], description = "Alias for the root path"
|
|
pub alias: Option<String>,
|
|
}
|
|
}
|
|
|
|
impl RootOptions {
|
|
pub fn new() -> RootOptions {
|
|
RootOptions {
|
|
min_depth: 0,
|
|
max_depth: 0,
|
|
archives: false,
|
|
symlinks: false,
|
|
gitignore: None,
|
|
hgignore: None,
|
|
dockerignore: None,
|
|
traversal: Bfs,
|
|
regexp: false,
|
|
alias: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn from(
|
|
min_depth: u32,
|
|
max_depth: u32,
|
|
archives: bool,
|
|
symlinks: bool,
|
|
gitignore: Option<bool>,
|
|
hgignore: Option<bool>,
|
|
dockerignore: Option<bool>,
|
|
traversal: TraversalMode,
|
|
regexp: bool,
|
|
alias: Option<String>,
|
|
) -> RootOptions {
|
|
RootOptions {
|
|
min_depth,
|
|
max_depth,
|
|
archives,
|
|
symlinks,
|
|
gitignore,
|
|
hgignore,
|
|
dockerignore,
|
|
traversal,
|
|
regexp,
|
|
alias,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Root {
|
|
pub fn new(path: String, options: RootOptions) -> Root {
|
|
Root { path, options, subquery: None }
|
|
}
|
|
|
|
pub fn from_subquery(subquery: Query, options: RootOptions) -> Root {
|
|
Root {
|
|
path: String::new(),
|
|
options,
|
|
subquery: Some(Box::new(subquery)),
|
|
}
|
|
}
|
|
|
|
pub fn default(options: Option<RootOptions>) -> Root {
|
|
Root {
|
|
path: String::from("."),
|
|
options: options.unwrap_or_else(RootOptions::new),
|
|
subquery: None,
|
|
}
|
|
}
|
|
|
|
pub fn clone_with_path(new_path: String, source: Root) -> Root {
|
|
Root {
|
|
path: new_path,
|
|
..source
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn is_subquery(&self) -> bool {
|
|
self.subquery.is_some()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Hash, Serialize)]
|
|
pub enum TraversalMode {
|
|
Bfs,
|
|
Dfs,
|
|
}
|
|
|
|
macro_rules! output_format {
|
|
(
|
|
$(#[$enum_attrs:meta])*
|
|
$vis:vis enum $enum_name:ident {
|
|
$(
|
|
@text = $text:literal
|
|
@description = $description:literal
|
|
$(#[$variant_attrs:meta])*
|
|
$(@supports_colorization = $colorized:literal)?
|
|
$variant:ident$(,)?
|
|
)*
|
|
}
|
|
) => {
|
|
$(#[$enum_attrs])*
|
|
$vis enum $enum_name {
|
|
$(
|
|
$(#[$variant_attrs])*
|
|
$variant,
|
|
)*
|
|
}
|
|
|
|
impl $enum_name {
|
|
pub fn from(s: &str) -> Option<OutputFormat> {
|
|
let s = s.to_lowercase();
|
|
match s.as_str() {
|
|
$(
|
|
$text => Some($enum_name::$variant),
|
|
)*
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_names_and_descriptions() -> Vec<(&'static str, &'static str)> {
|
|
vec![
|
|
$(
|
|
($text, $description),
|
|
)*
|
|
]
|
|
}
|
|
|
|
pub fn supports_colorization(&self) -> bool {
|
|
match self {
|
|
$(
|
|
$(#[$variant_attrs])*
|
|
$enum_name::$variant => {
|
|
false $(|| stringify!($colorized) == "true")?
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
output_format! {
|
|
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)]
|
|
pub enum OutputFormat {
|
|
@text = "tabs"
|
|
@description = "Tab-separated values (default)"
|
|
@supports_colorization = true
|
|
Tabs,
|
|
|
|
@text = "lines"
|
|
@description = "One item per line"
|
|
@supports_colorization = true
|
|
Lines,
|
|
|
|
@text = "list"
|
|
@description = "Entire output onto a single line for xargs"
|
|
List,
|
|
|
|
@text = "csv"
|
|
@description = "Comma-separated values"
|
|
Csv,
|
|
|
|
@text = "json"
|
|
@description = "JSON format"
|
|
Json,
|
|
|
|
@text = "html"
|
|
@description = "HTML format"
|
|
Html,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_supports_colorization_tabs() {
|
|
assert!(
|
|
OutputFormat::Tabs.supports_colorization(),
|
|
"Tabs should support colorization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_supports_colorization_lines() {
|
|
assert!(
|
|
OutputFormat::Lines.supports_colorization(),
|
|
"Lines should support colorization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_colorization_csv() {
|
|
assert!(
|
|
!OutputFormat::Csv.supports_colorization(),
|
|
"Csv should not support colorization"
|
|
);
|
|
}
|
|
} |