chore: import upstream snapshot with attribution
This commit is contained in:
+352
@@ -0,0 +1,352 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user