use std::path::PathBuf;
use clap::{ArgAction, ArgGroup, Args, Parser, Subcommand};
use clap_complete::Shell;
use super::help::{
CAPABILITIES_AFTER_LONG_HELP, CLI_FOOTER, CONV_AFTER_LONG_HELP, DEFAULT_SELECTOR_LONG_HELP, DIFF_AFTER_LONG_HELP,
INPUT_DATASET_LONG_HELP, INSPECT_AFTER_LONG_HELP, INSPECT_FORMAT_LONG_HELP,
PIVOT_AFTER_LONG_HELP, QUERY_AFTER_LONG_HELP, QUERY_INPUT_DATASET_LONG_HELP,
SCHEMA_AFTER_LONG_HELP, SCHEMA_FORMAT_LONG_HELP, TABLES_AFTER_LONG_HELP,
TABLES_FORMAT_LONG_HELP,
};
use super::{
HeaderCase, InspectionFormat, JsonMode, MetadataFormat, OutputFormat, XmlMode,
};
#[derive(Debug, Parser)]
#[command(
author = "Daniele Olmisani <daniele.olmisani@gmail.com>",
version = concat!(
env!("CARGO_PKG_VERSION"),
"\nADaniele Olmisani <daniele.olmisani@gmail.com>\nCrates.io: https://crates.io/crates/query-forge"
),
about = "Query and diff XLSX/XML/CSV/JSON/JSONL/Markdown/HTML/Feather/Parquet datasets"
)]
#[command(
after_help = CLI_FOOTER,
after_long_help = CLI_FOOTER
)]
pub(crate) struct Cli {
#[arg(
long = "ai-mode",
global = true,
long_help = "Emit command output as a machine-friendly JSON envelope.
Includes structured data, metadata, warnings, and stable error fields for AI tools.
Currently supported for: query, tables, schema, inspect, diff, pivot, conv."
)]
pub(crate) ai_mode: bool,
#[command(subcommand)]
pub(crate) command: Commands,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Commands {
#[command(after_long_help = QUERY_AFTER_LONG_HELP)]
Query(QueryCommand),
#[command(after_long_help = TABLES_AFTER_LONG_HELP)]
Tables(TablesCommand),
#[command(after_long_help = SCHEMA_AFTER_LONG_HELP)]
Schema(SchemaCommand),
#[command(after_long_help = INSPECT_AFTER_LONG_HELP)]
Inspect(InspectCommand),
#[command(after_long_help = DIFF_AFTER_LONG_HELP)]
Diff(DiffCommand),
#[command(after_long_help = PIVOT_AFTER_LONG_HELP)]
Pivot(PivotCommand),
#[command(after_long_help = CONV_AFTER_LONG_HELP)]
Conv(ConvCommand),
Completions(CompletionsCommand),
ManPage(ManPageCommand),
#[command(after_long_help = CAPABILITIES_AFTER_LONG_HELP)]
Capabilities(CapabilitiesCommand),
}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("query_source")
.required(true)
.args(["sql", "sql_file"])
))]
pub(crate) struct QueryCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = QUERY_INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
long,
alias = "query",
short = 'q',
conflicts_with = "sql_file",
value_name = "SQL",
long_help = "SQL query to execute. Use 'table', 'table2', 'table3', ... or explicit table names.\n\
The flag --query is also accepted as a legacy alias."
)]
pub(crate) sql: Option<String>,
#[arg(
long = "sql-file",
value_name = "PATH",
conflicts_with = "sql",
long_help = "Path to a file containing the SQL query to execute.\n\
The file is read as UTF-8 text and trimmed before execution."
)]
pub(crate) sql_file: Option<PathBuf>,
#[arg(
long = "infer-types",
conflicts_with = "all_text",
long_help = "Infer typed SQLite values from input data when possible.\n\
This is useful for numeric comparisons, boolean filtering, and date-aware ingestion."
)]
pub(crate) infer_types: bool,
#[arg(
long = "all-text",
conflicts_with = "infer_types",
long_help = "Load all incoming values as text instead of inferring typed SQLite values.\n\
Use this when you want exact string preservation or need to avoid automatic conversions."
)]
pub(crate) all_text: bool,
#[arg(
long = "decimal-comma",
long_help = "Interpret commas as decimal separators during type inference.\n\
Useful for locale-formatted values such as 12,50."
)]
pub(crate) decimal_comma: bool,
#[arg(
long = "date-format",
value_name = "STRFTIME",
long_help = "Expected date format used during type inference, for example %d/%m/%Y."
)]
pub(crate) date_format: Option<String>,
#[arg(
long = "null-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as NULL during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) null_values: Vec<String>,
#[arg(
long = "true-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean true during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) true_values: Vec<String>,
#[arg(
long = "false-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean false during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) false_values: Vec<String>,
#[arg(
long = "trim",
long_help = "Trim surrounding whitespace from every cell before loading it."
)]
pub(crate) trim: bool,
#[arg(
long = "skip-empty-rows",
long_help = "Discard rows whose values are entirely empty after normalization."
)]
pub(crate) skip_empty_rows: bool,
#[arg(
long = "normalize-headers",
long_help = "Normalize header names before loading them into SQLite.\n\
Combine with --header-case and --dedupe-headers for more stable schemas."
)]
pub(crate) normalize_headers: bool,
#[arg(
long = "header-case",
value_enum,
requires = "normalize_headers",
long_help = "Header naming style to apply after normalization.\n\
Requires --normalize-headers.\n\n\
snake — lowercase with underscores (e.g. product_id)\n\
camel — camelCase (e.g. productId)\n\
pascal — PascalCase (e.g. ProductId)\n\
screaming-snake — UPPER_SNAKE_CASE (e.g. PRODUCT_ID)"
)]
pub(crate) header_case: Option<HeaderCase>,
#[arg(
long = "dedupe-headers",
long_help = "Make duplicate normalized headers unique by appending numeric suffixes."
)]
pub(crate) dedupe_headers: bool,
#[arg(
long = "param",
value_name = "NAME=VALUE",
action = ArgAction::Append,
long_help = "Bind a named SQL parameter.\n\
Repeat the flag for multiple parameters; values are parsed as null, booleans, integers, reals, or text."
)]
pub(crate) params: Vec<String>,
#[arg(
short,
long,
value_name = "PATH",
long_help = "Write query results to a file instead of stdout.\n\
Pair with --format to choose the serialization format when the extension is ambiguous.\n\
Append |TYPE to force an explicit format (e.g. result.txt|json). The |TYPE suffix takes priority over the file extension.\n\
Use @clipboard or @clipboard|TYPE to copy text-based output to the system clipboard (e.g. @clipboard|csv, @clipboard|json, @clipboard|md)."
)]
pub(crate) output: Option<PathBuf>,
#[arg(
long,
value_enum,
long_help = "Output format for query results.\n\
Supported values: table (default), csv, json, jsonl, markdown (alias: md), html, xlsx, xml, feather, parquet.\n\
'table' is the default console-friendly format used when no other format is specified.\n\
'text' is accepted as an alias for 'table' (backward compatibility)."
)]
pub(crate) format: Option<OutputFormat>,
#[arg(
long = "no-headers",
long_help = "Treat the first row as data instead of column headers.\n\
Generated column names follow the pattern column1, column2, ..."
)]
pub(crate) no_headers: bool,
#[arg(
long = "json-mode",
value_enum,
long_help = "JSON extraction strategy.\n\
'array' (default): each element of a top-level JSON array becomes a row.\n\
'object': each key-value pair of a JSON object becomes a row with 'key' and 'value' columns.\n\
'flatten': recursively flatten nested JSON objects and arrays using dotted key paths."
)]
pub(crate) json_mode: Option<JsonMode>,
#[arg(
long = "xml-mode",
value_enum,
long_help = "XML extraction strategy.\n\
'rows' (default): detect and extract tabular rows from the XML structure.\n\
'descendants': collect every leaf text element as a row with 'tag' and 'value' columns.\n\
'attributes': extract element attributes as columns; each element with attributes becomes a row."
)]
pub(crate) xml_mode: Option<XmlMode>,
#[arg(
long,
long_help = "Print query execution metadata to stderr after running the query.\n\
Includes row count, output column names, input tables loaded, and execution time.\n\
Use --meta-format to choose between text (default) and json output."
)]
pub(crate) meta: bool,
#[arg(
long = "meta-format",
value_enum,
requires = "meta",
long_help = "Format for metadata output printed to stderr.\n\
Supported values: text (default), json.\n\
Requires --meta."
)]
pub(crate) meta_format: Option<MetadataFormat>,
#[arg(
long = "with",
value_name = "CTE_DEF",
action = ArgAction::Append,
long_help = "Prepend a CTE definition before the main query.\n\
The value should be 'name AS (...)' without the leading WITH keyword.\n\
Repeat the flag for multiple CTEs.\n\n\
Example:\n\
--with 'active AS (SELECT * FROM table WHERE status = 1)' \\\n\
--sql 'SELECT name FROM active'"
)]
pub(crate) with_ctes: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct TablesCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
short,
long,
value_name = "SELECTOR",
long_help = DEFAULT_SELECTOR_LONG_HELP
)]
pub(crate) sheet: Option<String>,
#[arg(
long,
value_enum,
default_value = "text",
long_help = TABLES_FORMAT_LONG_HELP
)]
pub(crate) format: InspectionFormat,
}
#[derive(Debug, Args)]
pub(crate) struct SchemaCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
short,
long,
value_name = "SELECTOR",
long_help = DEFAULT_SELECTOR_LONG_HELP
)]
pub(crate) sheet: Option<String>,
#[arg(
long,
value_enum,
default_value = "text",
long_help = SCHEMA_FORMAT_LONG_HELP
)]
pub(crate) format: InspectionFormat,
}
#[derive(Debug, Args)]
pub(crate) struct InspectCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
short,
long,
value_name = "SELECTOR",
long_help = DEFAULT_SELECTOR_LONG_HELP
)]
pub(crate) sheet: Option<String>,
#[arg(
long,
value_enum,
default_value = "text",
long_help = INSPECT_FORMAT_LONG_HELP
)]
pub(crate) format: InspectionFormat,
#[arg(
long,
default_value = "5",
long_help = "Number of sample rows to include per table in the inspection output."
)]
pub(crate) sample: usize,
#[arg(
long,
long_help = "Include additional metrics such as distinct counts and numeric min/max values when available."
)]
pub(crate) stats: bool,
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub(crate) enum DiffShow {
Added,
Removed,
Changed,
Unchanged,
All,
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub(crate) enum DiffOutputFormat {
Table,
Text,
Csv,
Json,
Jsonl,
#[value(alias = "md")]
Markdown,
Html,
}
#[derive(Debug, Args)]
pub(crate) struct DiffCommand {
#[arg(value_name = "[NAME=]PATH[:SHEET|KEY]")]
pub(crate) left: String,
#[arg(value_name = "[NAME=]PATH[:SHEET|KEY]")]
pub(crate) right: String,
#[arg(
long = "key",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "COL[,COL2,...]",
long_help = "One or more columns that identify rows across inputs.\n\
Without --key, rows are compared positionally."
)]
pub(crate) key: Vec<String>,
#[arg(
long = "show",
value_enum,
value_delimiter = ',',
action = ArgAction::Append,
default_value = "added,removed,changed",
value_name = "added|removed|changed|unchanged|all",
long_help = "Select which change classes to emit. Default: added,removed,changed."
)]
pub(crate) show: Vec<DiffShow>,
#[arg(
long = "ignore-columns",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "COL[,COL2,...]",
long_help = "Columns to exclude from value comparison.\n\
Ignored columns can still appear in output."
)]
pub(crate) ignore_columns: Vec<String>,
#[arg(
long = "schema-only",
long_help = "Compare only column structure and inferred types, not row-level data."
)]
pub(crate) schema_only: bool,
#[arg(
long = "side-by-side",
long_help = "Emit a single row per compared row with paired _left_* and _right_* columns."
)]
pub(crate) side_by_side: bool,
#[arg(
long = "infer-types",
conflicts_with = "all_text",
long_help = "Infer typed SQLite values from input data when possible."
)]
pub(crate) infer_types: bool,
#[arg(
long = "all-text",
conflicts_with = "infer_types",
long_help = "Load all incoming values as text instead of inferring typed SQLite values."
)]
pub(crate) all_text: bool,
#[arg(
long = "decimal-comma",
long_help = "Interpret commas as decimal separators during type inference."
)]
pub(crate) decimal_comma: bool,
#[arg(
long = "date-format",
value_name = "STRFTIME",
long_help = "Expected date format used during type inference, for example %d/%m/%Y."
)]
pub(crate) date_format: Option<String>,
#[arg(
long = "null-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as NULL during type inference."
)]
pub(crate) null_values: Vec<String>,
#[arg(
long = "true-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean true during type inference."
)]
pub(crate) true_values: Vec<String>,
#[arg(
long = "false-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean false during type inference."
)]
pub(crate) false_values: Vec<String>,
#[arg(
long = "trim",
long_help = "Trim surrounding whitespace from every cell before loading it."
)]
pub(crate) trim: bool,
#[arg(
long = "skip-empty-rows",
long_help = "Discard rows whose values are entirely empty after normalization."
)]
pub(crate) skip_empty_rows: bool,
#[arg(
long = "normalize-headers",
long_help = "Normalize header names before loading them into SQLite."
)]
pub(crate) normalize_headers: bool,
#[arg(
long = "header-case",
value_enum,
requires = "normalize_headers",
long_help = "Header naming style to apply after normalization.\n\
Requires --normalize-headers."
)]
pub(crate) header_case: Option<HeaderCase>,
#[arg(
long = "dedupe-headers",
long_help = "Make duplicate normalized headers unique by appending numeric suffixes."
)]
pub(crate) dedupe_headers: bool,
#[arg(
long = "json-mode",
value_enum,
long_help = "JSON extraction strategy: array (default), object, flatten."
)]
pub(crate) json_mode: Option<JsonMode>,
#[arg(
long = "xml-mode",
value_enum,
long_help = "XML extraction strategy: rows (default), descendants, attributes."
)]
pub(crate) xml_mode: Option<XmlMode>,
#[arg(
long = "no-headers",
long_help = "Treat the first row as data instead of column headers."
)]
pub(crate) no_headers: bool,
#[arg(
short,
long,
value_name = "PATH",
long_help = "Write diff results to a file instead of stdout.\n\
Append |TYPE to force an explicit format (e.g. result.txt|json). The |TYPE suffix takes priority over the file extension.\n\
Use @clipboard or @clipboard|TYPE to copy text-based output to the system clipboard (e.g. @clipboard|csv, @clipboard|json, @clipboard|md)."
)]
pub(crate) output: Option<PathBuf>,
#[arg(
long,
value_enum,
long_help = "Output format for diff results.\n\
Supported values: table (default), csv, json, jsonl, markdown (alias: md), html.\n\
'table' is the default console-friendly format used when no other format is specified.\n\
'text' is accepted as an alias for 'table' (backward compatibility)."
)]
pub(crate) format: Option<DiffOutputFormat>,
}
#[derive(Debug, Args)]
pub(crate) struct CompletionsCommand {
#[arg(value_enum)]
pub(crate) shell: Shell,
}
#[derive(Debug, Args)]
pub(crate) struct ManPageCommand {
#[arg(long, value_name = "PATH")]
pub(crate) output: Option<PathBuf>,
}
#[derive(Debug, Args)]
pub(crate) struct CapabilitiesCommand {}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("pivot_query_source")
.required(true)
.args(["query", "query_file"])
))]
pub(crate) struct PivotCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = QUERY_INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
long,
alias = "sql",
short = 'q',
conflicts_with = "query_file",
value_name = "PIVOT_SQL",
long_help = "Pivot query in the pivot SQL mini-language.\n\
Syntax: PIVOT AGG_FN(col | *) [FOR cols_col] FROM table_name GROUP BY rows_col\n\
\n\
Examples:\n\
PIVOT COUNT(*) FROM table GROUP BY category\n\
PIVOT SUM(stock) FOR active FROM table GROUP BY category\n\
PIVOT AVG(price) FOR region FROM sales GROUP BY product\n\
\n\
AGG_FN must be one of: COUNT, SUM, AVG, MIN, MAX (case-insensitive).\n\
Use * only with COUNT; all other functions require a column name.\n\
FOR cols_col is optional — when omitted a single aggregated column is produced.\n\
Table and column names may be double-quoted for names that contain spaces or keywords."
)]
pub(crate) query: Option<String>,
#[arg(
long = "query-file",
alias = "sql-file",
conflicts_with = "query",
value_name = "PATH",
long_help = "Path to a file containing a pivot query written in the pivot SQL mini-language.\n\
The file is read as UTF-8 text and trimmed before parsing.\n\
A trailing semicolon at the end of the file is ignored.\n\
See --query for the full syntax reference."
)]
pub(crate) query_file: Option<PathBuf>,
#[arg(
short,
long,
value_name = "PATH",
long_help = "Write pivot results to a file instead of stdout.\n\
Pair with --format to choose the serialization format when the extension is ambiguous.\n\
Append |TYPE to force an explicit format (e.g. result.txt|json). The |TYPE suffix takes priority over the file extension.\n\
Use @clipboard or @clipboard|TYPE to copy text-based output to the system clipboard."
)]
pub(crate) output: Option<PathBuf>,
#[arg(
long,
value_enum,
long_help = "Output format for pivot results.\n\
Supported values: table (default), csv, json, jsonl, markdown (alias: md), html, xml, xlsx, feather, parquet.\n\
The pivot table is always computed as static data via SQL (CASE WHEN crosstab).\n\
All formats, including xlsx, receive the pre-computed static result."
)]
pub(crate) format: Option<OutputFormat>,
#[arg(
long = "show-sql",
long_help = "Print the generated pivot SQL to stderr before executing it.\n\
Useful for debugging the crosstab query or reusing it in other tools."
)]
pub(crate) show_sql: bool,
#[arg(
long = "infer-types",
conflicts_with = "all_text",
long_help = "Infer typed SQLite values from input data when possible."
)]
pub(crate) infer_types: bool,
#[arg(
long = "all-text",
conflicts_with = "infer_types",
long_help = "Load all incoming values as text instead of inferring typed SQLite values."
)]
pub(crate) all_text: bool,
#[arg(
long = "decimal-comma",
long_help = "Interpret commas as decimal separators during type inference."
)]
pub(crate) decimal_comma: bool,
#[arg(
long = "date-format",
value_name = "STRFTIME",
long_help = "Expected date format used during type inference, for example %d/%m/%Y."
)]
pub(crate) date_format: Option<String>,
#[arg(
long = "null-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as NULL during type inference."
)]
pub(crate) null_values: Vec<String>,
#[arg(
long = "true-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean true during type inference."
)]
pub(crate) true_values: Vec<String>,
#[arg(
long = "false-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean false during type inference."
)]
pub(crate) false_values: Vec<String>,
#[arg(long = "trim", long_help = "Trim surrounding whitespace from every cell before loading it.")]
pub(crate) trim: bool,
#[arg(
long = "skip-empty-rows",
long_help = "Discard rows whose values are entirely empty after normalization."
)]
pub(crate) skip_empty_rows: bool,
#[arg(
long = "normalize-headers",
long_help = "Normalize header names before loading them into SQLite."
)]
pub(crate) normalize_headers: bool,
#[arg(
long = "header-case",
value_enum,
requires = "normalize_headers",
long_help = "Header naming style to apply after normalization. Requires --normalize-headers."
)]
pub(crate) header_case: Option<HeaderCase>,
#[arg(
long = "dedupe-headers",
long_help = "Make duplicate normalized headers unique by appending numeric suffixes."
)]
pub(crate) dedupe_headers: bool,
#[arg(
long = "json-mode",
value_enum,
long_help = "JSON extraction strategy: array (default), object, flatten."
)]
pub(crate) json_mode: Option<JsonMode>,
#[arg(
long = "xml-mode",
value_enum,
long_help = "XML extraction strategy: rows (default), descendants, attributes."
)]
pub(crate) xml_mode: Option<XmlMode>,
#[arg(
long = "no-headers",
long_help = "Treat the first row as data instead of column headers."
)]
pub(crate) no_headers: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ConvCommand {
#[arg(
short,
long,
required = true,
action = ArgAction::Append,
value_name = "[NAME=]PATH[:SHEET|KEY]",
long_help = INPUT_DATASET_LONG_HELP
)]
pub(crate) input: Vec<String>,
#[arg(
short,
long,
value_name = "PATH",
long_help = "Write conversion results to a file instead of stdout.\n\
Pair with --format to choose the serialization format when the extension is ambiguous.\n\
Append |TYPE to force an explicit format (e.g. result.txt|json). The |TYPE suffix takes priority over the file extension.\n\
Use @clipboard or @clipboard|TYPE to copy text-based output to the system clipboard (e.g. @clipboard|csv, @clipboard|json, @clipboard|md)."
)]
pub(crate) output: Option<PathBuf>,
#[arg(
long,
value_enum,
long_help = "Output format for conversion results.\n\
Supported values: table (default), csv, json, jsonl, markdown (alias: md), html, xlsx, xml, feather, parquet.\n\
'table' is the default console-friendly format used when no other format is specified.\n\
'text' is accepted as an alias for 'table' (backward compatibility)."
)]
pub(crate) format: Option<OutputFormat>,
#[arg(
long = "no-headers",
long_help = "Treat the first row as data instead of column headers.\n\
Generated column names follow the pattern column1, column2, ..."
)]
pub(crate) no_headers: bool,
#[arg(
long = "infer-types",
conflicts_with = "all_text",
long_help = "Infer typed SQLite values from input data when possible."
)]
pub(crate) infer_types: bool,
#[arg(
long = "all-text",
conflicts_with = "infer_types",
long_help = "Load all incoming values as text instead of inferring typed SQLite values."
)]
pub(crate) all_text: bool,
#[arg(
long = "decimal-comma",
long_help = "Interpret commas as decimal separators during type inference."
)]
pub(crate) decimal_comma: bool,
#[arg(
long = "date-format",
value_name = "STRFTIME",
long_help = "Expected date format used during type inference, for example %d/%m/%Y."
)]
pub(crate) date_format: Option<String>,
#[arg(
long = "null-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as NULL during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) null_values: Vec<String>,
#[arg(
long = "true-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean true during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) true_values: Vec<String>,
#[arg(
long = "false-values",
value_delimiter = ',',
action = ArgAction::Append,
value_name = "VALUE[,VALUE...]",
long_help = "Additional strings to interpret as boolean false during type inference.\n\
Repeat the flag or pass comma-separated values."
)]
pub(crate) false_values: Vec<String>,
#[arg(long = "trim", long_help = "Trim surrounding whitespace from every cell before loading it.")]
pub(crate) trim: bool,
#[arg(
long = "skip-empty-rows",
long_help = "Discard rows whose values are entirely empty after normalization."
)]
pub(crate) skip_empty_rows: bool,
#[arg(
long = "normalize-headers",
long_help = "Normalize header names before loading them into SQLite.\n\
Combine with --header-case and --dedupe-headers for more stable schemas."
)]
pub(crate) normalize_headers: bool,
#[arg(
long = "header-case",
value_enum,
requires = "normalize_headers",
long_help = "Header naming style to apply after normalization. Requires --normalize-headers."
)]
pub(crate) header_case: Option<HeaderCase>,
#[arg(
long = "dedupe-headers",
long_help = "Make duplicate normalized headers unique by appending numeric suffixes."
)]
pub(crate) dedupe_headers: bool,
#[arg(
long = "json-mode",
value_enum,
long_help = "JSON extraction strategy: array (default), object, flatten."
)]
pub(crate) json_mode: Option<JsonMode>,
#[arg(
long = "xml-mode",
value_enum,
long_help = "XML extraction strategy: rows (default), descendants, attributes."
)]
pub(crate) xml_mode: Option<XmlMode>,
}