diff --git a/Cargo.lock b/Cargo.lock index 5cce8efcf2..4e91536041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1254,6 +1254,7 @@ dependencies = [ "sqlbuild-analysis", "sqlbuild-attachments", "sqlbuild-cache", + "sqlbuild-cli", "sqlbuild-config", "sqlbuild-core", "sqlbuild-discovery", diff --git a/Cargo.toml b/Cargo.toml index cb65e46fc8..2f981266b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ sha2 = "0.10" sqlbuild-analysis = { path = "crates/sqlbuild-analysis" } sqlbuild-attachments = { path = "crates/sqlbuild-attachments" } sqlbuild-cache = { path = "crates/sqlbuild-cache" } +sqlbuild-cli = { path = "crates/sqlbuild-cli" } sqlbuild-config = { path = "crates/sqlbuild-config" } sqlbuild-core = { path = "crates/sqlbuild-core" } sqlbuild-discovery = { path = "crates/sqlbuild-discovery" } diff --git a/crates/sqlbuild-cli/src/argv/_helpers/formatting.rs b/crates/sqlbuild-cli/src/argv/_helpers/formatting.rs new file mode 100644 index 0000000000..de75e3bad9 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/formatting.rs @@ -0,0 +1,437 @@ +//! `argparse.HelpFormatter`'s help, usage and version text in the Python 3.13 layout. + +use crate::argv::_helpers::text_wrap::{normalize_whitespace, wrap}; +use crate::argv::constants::{ + INDENT_INCREMENT, MAX_HELP_POSITION, MAXIMUM_BLANK_RUN, MINIMUM_TEXT_WIDTH, USAGE_PREFIX, + VERSION_PLACEHOLDER, +}; +use crate::argv::models::{ActionSpec, ArgvSettings, HelpText, Nargs, ParserSpec}; + +/// One formatter's widths: `HelpFormatter.__init__` and the longest invocation it has seen. +struct Formatter { + width: i64, + max_help_position: i64, + action_max_length: i64, +} + +impl Formatter { + fn new(settings: &ArgvSettings) -> Self { + let width: i64 = settings.columns - 2; + Self { + width, + max_help_position: MAX_HELP_POSITION.min((width - 20).max(INDENT_INCREMENT * 2)), + action_max_length: 0, + } + } +} + +/// `parser.format_usage()`. +pub(crate) fn format_usage(settings: &ArgvSettings, parser: &ParserSpec) -> String { + let formatter: Formatter = Formatter::new(settings); + finish(&[usage(&formatter, parser)]) +} + +/// The `--version` action's output. +pub(crate) fn format_version( + settings: &ArgvSettings, + action: &ActionSpec, + version: &str, +) -> String { + let formatter: Formatter = Formatter::new(settings); + let text: String = action + .version + .as_deref() + .unwrap_or_default() + .replace(VERSION_PLACEHOLDER, version); + finish(&[format_text(&formatter, &text, 0)]) +} + +/// `parser.format_help()`. +pub(crate) fn format_help(settings: &ArgvSettings, parser: &ParserSpec) -> String { + let mut formatter: Formatter = Formatter::new(settings); + for action in parser + .actions + .iter() + .filter(|action| action.help != HelpText::Suppress) + { + let entry: HelpEntry = help_entry(action); + let mut length: i64 = entry.header.chars().count() as i64 + INDENT_INCREMENT; + for subaction in &entry.subactions { + length = length.max(subaction.header.chars().count() as i64 + 2 * INDENT_INCREMENT); + } + formatter.action_max_length = formatter.action_max_length.max(length); + } + let mut items: Vec = vec![usage(&formatter, parser)]; + if let Some(description) = &parser.description { + items.push(format_text(&formatter, description, 0)); + } + for (index, group) in parser.groups.iter().enumerate() { + let mut section: Vec = Vec::new(); + if let Some(description) = &group.description { + section.push(format_text(&formatter, description, INDENT_INCREMENT)); + } + for action in parser + .actions + .iter() + .filter(|action| action.group == index && action.help != HelpText::Suppress) + { + section.push(format_action( + &formatter, + &help_entry(action), + INDENT_INCREMENT, + )); + } + let body: String = section.concat(); + if body.is_empty() { + continue; + } + let heading: String = group + .title + .as_ref() + .map_or_else(String::new, |title| format!("{title}:\n")); + items.push(format!("\n{heading}{body}\n")); + } + if let Some(epilog) = &parser.epilog { + items.push(format_text(&formatter, epilog, 0)); + } + finish(&items) +} + +/// The root section's `format_help` followed by `HelpFormatter.format_help`'s clean-up. +fn finish(items: &[String]) -> String { + let body: String = items.concat(); + if body.is_empty() { + return String::new(); + } + let mut text: String = String::with_capacity(body.len() + 2); + let mut newlines: usize = 0; + for character in format!("\n{body}\n").chars() { + if character == '\n' { + newlines += 1; + if newlines > MAXIMUM_BLANK_RUN { + continue; + } + } else { + newlines = 0; + } + text.push(character); + } + format!("{}\n", text.trim_matches('\n')) +} + +fn help_text(help: &HelpText) -> Option<&str> { + match help { + HelpText::Text(text) => Some(text), + HelpText::None | HelpText::Suppress => None, + } +} + +/// `_format_text`: a filled paragraph at `indent`. +fn format_text(formatter: &Formatter, text: &str, indent: i64) -> String { + let width: i64 = (formatter.width - indent).max(MINIMUM_TEXT_WIDTH); + let padding: String = " ".repeat(indent as usize); + let lines: Vec = wrap(&normalize_whitespace(text), width as usize, &padding); + format!("{}\n\n", lines.join("\n")) +} + +/// One help line's invocation and text, with its indented sub-actions. +struct HelpEntry { + header: String, + help: Option, + subactions: Vec, +} + +/// An action's help entry; `_iter_indented_subactions` gives a sub-command action's choices. +fn help_entry(action: &ActionSpec) -> HelpEntry { + HelpEntry { + header: invocation(action), + help: help_text(&action.help).map(str::to_owned), + subactions: action + .choices_help + .iter() + .map(|choice| HelpEntry { + header: choice.metavar.clone(), + help: help_text(&choice.help).map(str::to_owned), + subactions: Vec::new(), + }) + .collect(), + } +} + +/// `_format_action`: one action's help entry and its sub-actions'. +fn format_action(formatter: &Formatter, entry: &HelpEntry, indent: i64) -> String { + let help_position: i64 = (formatter.action_max_length + 2).min(formatter.max_help_position); + let help_width: i64 = (formatter.width - help_position).max(MINIMUM_TEXT_WIDTH); + let action_width: i64 = help_position - indent - 2; + let padding: String = " ".repeat(indent as usize); + let header: &str = &entry.header; + let help: Option<&str> = entry.help.as_deref().filter(|text| !text.is_empty()); + let header_length: i64 = header.chars().count() as i64; + let mut indent_first: i64 = 0; + let mut text: String = match help { + None => format!("{padding}{header}\n"), + Some(_) if header_length <= action_width => { + format!("{padding}{header: { + indent_first = help_position; + format!("{padding}{header}\n") + } + }; + let lines: Vec = help + .map(|help| wrap(&normalize_whitespace(help), help_width as usize, "")) + .unwrap_or_default(); + if let Some((first, rest)) = lines.split_first() { + text.push_str(&format!("{}{first}\n", " ".repeat(indent_first as usize))); + for line in rest { + text.push_str(&format!("{}{line}\n", " ".repeat(help_position as usize))); + } + } else if !text.ends_with('\n') { + text.push('\n'); + } + for subaction in &entry.subactions { + text.push_str(&format_action( + formatter, + subaction, + indent + INDENT_INCREMENT, + )); + } + text +} + +/// `_metavar_formatter(action, default)(1)[0]`. +fn metavar(action: &ActionSpec, default: &str) -> String { + match (&action.metavar, &action.choices) { + (Some(Some(metavar)), _) => metavar.clone(), + (Some(None), _) => crate::argv::constants::SUPPRESS_DEST.to_owned(), + (None, Some(choices)) => format!("{{{}}}", choices.join(",")), + (None, None) => default.to_owned(), + } +} + +/// `_format_args`. +fn format_args(action: &ActionSpec, default: &str) -> String { + let name: String = metavar(action, default); + match action.nargs { + Nargs::Single => name, + Nargs::Optional => format!("[{name}]"), + Nargs::ZeroOrMore => format!("[{name} ...]"), + Nargs::OneOrMore => format!("{name} [{name} ...]"), + Nargs::Parser => format!("{name} ..."), + Nargs::Zero => String::new(), + } +} + +fn dest(action: &ActionSpec) -> &str { + action + .dest + .as_deref() + .unwrap_or(crate::argv::constants::SUPPRESS_DEST) +} + +/// `_format_action_invocation`. +fn invocation(action: &ActionSpec) -> String { + if action.option_strings.is_empty() { + return metavar(action, dest(action)); + } + let options: String = action.option_strings.join(", "); + if action.nargs == Nargs::Zero { + return options; + } + format!( + "{options} {}", + format_args(action, &dest(action).to_uppercase()) + ) +} + +/// `_format_usage`: the `usage:` line, wrapped to the terminal width. +fn usage(formatter: &Formatter, parser: &ParserSpec) -> String { + let prog: &str = &parser.prog; + if let Some(usage) = &parser.usage { + return format!("{USAGE_PREFIX}{}\n\n", usage.replace("%(prog)s", prog)); + } + if parser.actions.is_empty() { + return format!("{USAGE_PREFIX}{prog}\n\n"); + } + let mut ordered: Vec<&ActionSpec> = parser + .actions + .iter() + .filter(|action| !action.option_strings.is_empty()) + .collect(); + let optional_count: usize = ordered.len(); + ordered.extend( + parser + .actions + .iter() + .filter(|action| action.option_strings.is_empty()), + ); + let (parts, positional_start) = usage_parts(parser, &ordered, optional_count); + let action_usage: String = parts.join(" "); + let text: String = [prog, action_usage.as_str()] + .iter() + .filter(|part| !part.is_empty()) + .copied() + .collect::>() + .join(" "); + let prefix_length: usize = USAGE_PREFIX.len(); + let text_width: i64 = formatter.width; + if (prefix_length + text.chars().count()) as i64 <= text_width { + return format!("{USAGE_PREFIX}{text}\n\n"); + } + let optional_parts: &[String] = &parts[..positional_start]; + let positional_parts: &[String] = &parts[positional_start..]; + let prog_length: usize = prog.chars().count(); + let lines: Vec = if (prefix_length + prog_length) as f64 <= 0.75 * text_width as f64 { + let indent: String = " ".repeat(prefix_length + prog_length + 1); + let with_prog = |parts: &[String]| -> Vec { + let mut all: Vec = vec![prog.to_owned()]; + all.extend(parts.iter().cloned()); + usage_lines(&all, &indent, Some(prefix_length), text_width) + }; + if !optional_parts.is_empty() { + let mut lines: Vec = with_prog(optional_parts); + lines.extend(usage_lines(positional_parts, &indent, None, text_width)); + lines + } else if !positional_parts.is_empty() { + with_prog(positional_parts) + } else { + vec![prog.to_owned()] + } + } else { + let indent: String = " ".repeat(prefix_length); + let mut lines: Vec = usage_lines(&parts, &indent, None, text_width); + if lines.len() > 1 { + lines = usage_lines(optional_parts, &indent, None, text_width); + lines.extend(usage_lines(positional_parts, &indent, None, text_width)); + } + let mut all: Vec = vec![prog.to_owned()]; + all.extend(lines); + all + }; + format!("{USAGE_PREFIX}{}\n\n", lines.join("\n")) +} + +/// `get_lines` inside `_format_usage`. +fn usage_lines( + parts: &[String], + indent: &str, + prefix_length: Option, + text_width: i64, +) -> Vec { + let indent_length: i64 = indent.chars().count() as i64; + let mut lines: Vec = Vec::new(); + let mut line: Vec<&str> = Vec::new(); + let mut line_length: i64 = prefix_length.map_or(indent_length, |length| length as i64) - 1; + for part in parts { + let part_length: i64 = part.chars().count() as i64; + if line_length + 1 + part_length > text_width && !line.is_empty() { + lines.push(format!("{indent}{}", line.join(" "))); + line.clear(); + line_length = indent_length - 1; + } + line.push(part); + line_length += part_length + 1; + } + if !line.is_empty() { + lines.push(format!("{indent}{}", line.join(" "))); + } + if prefix_length.is_some() + && let Some(first) = lines.first_mut() + { + *first = first.chars().skip(indent_length as usize).collect(); + } + lines +} + +/// `_get_actions_usage_parts_with_split`: usage parts and where the positionals start. +fn usage_parts( + parser: &ParserSpec, + actions: &[&ActionSpec], + optional_count: usize, +) -> (Vec, usize) { + let mut grouped: Vec = Vec::new(); + let mut inserts: Vec<(usize, usize, bool)> = Vec::new(); + for (group, required) in parser.mutex_groups.iter().enumerate() { + let members: Vec<&ActionSpec> = parser + .actions + .iter() + .filter(|action| action.mutex_group == Some(group)) + .collect(); + if members + .iter() + .all(|action| action.help == HelpText::Suppress) + { + continue; + } + let Some(start) = actions.iter().position(|action| action.id == members[0].id) else { + continue; + }; + let end: usize = start + members.len(); + let contiguous: bool = end <= actions.len() + && actions[start..end] + .iter() + .zip(&members) + .all(|(action, member)| action.id == member.id); + if contiguous { + grouped.extend(members.iter().map(|action| action.id)); + inserts.push((start, end, *required)); + } + } + let mut parts: Vec> = actions + .iter() + .map(|action| usage_part(action, grouped.contains(&action.id))) + .collect(); + inserts.sort_by_key(|insert| std::cmp::Reverse((insert.0, insert.1))); + let mut separated: Vec = Vec::new(); + for (start, end, required) in inserts { + let mut group_parts: Vec = parts[start..end].iter().flatten().cloned().collect(); + let size: usize = group_parts.len(); + let (open, close) = match (required, size > 1) { + (true, true) => ("(", ")"), + (true, false) => ("", ""), + (false, _) => ("[", "]"), + }; + group_parts[0] = format!("{open}{}", group_parts[0]); + group_parts[size - 1] = format!("{}{close}", group_parts[size - 1]); + for (offset, part) in group_parts[..size - 1].iter().enumerate() { + let index: usize = start + offset; + if !separated.contains(&index) { + parts[index] = Some(format!("{part} |")); + separated.push(index); + } + } + parts[start + size - 1] = Some(group_parts[size - 1].clone()); + for part in &mut parts[start + size..end] { + *part = None; + } + } + let positional_start: usize = parts[..optional_count].iter().flatten().count(); + (parts.into_iter().flatten().collect(), positional_start) +} + +/// One action's usage part, or `None` when its help is suppressed. +fn usage_part(action: &ActionSpec, grouped: bool) -> Option { + if action.help == HelpText::Suppress { + return None; + } + if action.option_strings.is_empty() { + let part: String = format_args(action, dest(action)); + if grouped && part.starts_with('[') && part.ends_with(']') { + return Some(part[1..part.len() - 1].to_owned()); + } + return Some(part); + } + let option: &str = &action.option_strings[0]; + let part: String = if action.nargs == Nargs::Zero { + option.to_owned() + } else { + format!( + "{option} {}", + format_args(action, &dest(action).to_uppercase()) + ) + }; + if !action.required && !grouped { + return Some(format!("[{part}]")); + } + Some(part) +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/mod.rs b/crates/sqlbuild-cli/src/argv/_helpers/mod.rs new file mode 100644 index 0000000000..4575f034fb --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/mod.rs @@ -0,0 +1,8 @@ +pub(crate) mod formatting; +pub(crate) mod namespace; +pub(crate) mod normalization; +pub(crate) mod parsing; +pub(crate) mod patterns; +pub(crate) mod table; +pub(crate) mod text_wrap; +pub(crate) mod values; diff --git a/crates/sqlbuild-cli/src/argv/_helpers/namespace.rs b/crates/sqlbuild-cli/src/argv/_helpers/namespace.rs new file mode 100644 index 0000000000..945cfd135b --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/namespace.rs @@ -0,0 +1,31 @@ +//! `argparse.Namespace` attribute access. + +use crate::argv::models::{Namespace, Value}; + +impl Namespace { + /// `getattr(namespace, name)`, when the attribute exists. + pub(crate) fn get(&self, name: &str) -> Option<&Value> { + self.entries + .iter() + .find(|(entry, _)| entry == name) + .map(|(_, value)| value) + } + + /// `hasattr(namespace, name)`. + pub(crate) fn has(&self, name: &str) -> bool { + self.get(name).is_some() + } + + /// `setattr(namespace, name, value)`, keeping the first assignment's position. + pub(crate) fn set(&mut self, name: &str, value: Value) { + match self.entries.iter_mut().find(|(entry, _)| entry == name) { + Some((_, existing)) => *existing = value, + None => self.entries.push((name.to_owned(), value)), + } + } + + /// The attribute's value, or `None` when it is missing. + pub(crate) fn value(&self, name: &str) -> Value { + self.get(name).cloned().unwrap_or(Value::None) + } +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/normalization.rs b/crates/sqlbuild-cli/src/argv/_helpers/normalization.rs new file mode 100644 index 0000000000..f446d3496b --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/normalization.rs @@ -0,0 +1,251 @@ +//! `parse_cli_invocation`'s rules after `parse_known_args`: passthrough, selectors and validation. + +use crate::argv::_helpers::parsing::{Exit, KnownArgs, Run}; +use crate::argv::constants::{ + COMMAND_DEST, CONCURRENCY_COMMANDS, CONCURRENCY_ENV_VAR, CURSOR_COMMANDS, DBT_COMMAND, + DBT_DEBUG_COMMAND, DBT_PASSTHROUGH_COMMANDS, DBT_PASSTHROUGH_SEPARATOR, DBT_VERBOSE_OPTION, + DBT_VERBOSE_OPTIONS, DEBUG_OPTION, INT_CONVERTER, JANITOR_COMMAND, NO_COLOR_OPTION, + POSITIONAL_SELECT_COMMANDS, SCOPE_COMMAND, SCOPE_DEFAULT_PAGE_SIZE, SCOPE_GLOBAL_SUMMARY, + SCOPE_KIND_DEST, TEST_COMMAND, +}; +use crate::argv::models::{Namespace, ParserSpec, Value}; + +impl Run<'_> { + /// Apply the post-parse rules to `namespace`, or exit through `parser.error`. + pub(crate) fn normalized( + &mut self, + parser: &ParserSpec, + (mut namespace, unknown): KnownArgs, + ) -> Result { + let command: Option = text(&namespace.value(COMMAND_DEST)); + let command: &str = command.as_deref().unwrap_or_default(); + let dbt_command: Option = text(&namespace.value("dbt_command")); + if command == DBT_COMMAND + && dbt_command + .as_deref() + .is_some_and(|name| DBT_PASSTHROUGH_COMMANDS.contains(&name)) + { + let mut passthrough: Vec = Vec::new(); + let mut started: bool = false; + for arg in unknown { + if arg == DBT_PASSTHROUGH_SEPARATOR { + started = true; + } else if arg == NO_COLOR_OPTION && !started { + namespace.set("no_color", Value::Bool(true)); + continue; + } else if arg == DEBUG_OPTION && !started { + namespace.set("debug", Value::Bool(true)); + continue; + } + passthrough.push(Value::Str(arg)); + } + namespace.set("dbt_args", Value::List(passthrough)); + } else { + let mut unknown: Vec = unknown; + if POSITIONAL_SELECT_COMMANDS.contains(&command) { + let mut select: Vec = list(&namespace.value("positional_select")); + if !unknown.is_empty() && !unknown.iter().any(|arg| arg.starts_with('-')) { + select.extend(unknown.drain(..).map(Value::Str)); + } + select.extend(list(&namespace.value("select"))); + namespace.set("select", Value::List(select)); + } + if !unknown.is_empty() { + return Err(self.error( + parser, + &format!("unrecognized arguments: {}", unknown.join(" ")), + )); + } + } + let verbose: bool = + truthy(&namespace.value("verbose")) || truthy(&namespace.value("debug")); + namespace.set("verbose", Value::Bool(verbose)); + let mut dbt_args: Vec = list(&namespace.value("dbt_args")); + if truthy(&namespace.value("debug")) + && command == DBT_COMMAND + && dbt_command.as_deref() != Some(DBT_DEBUG_COMMAND) + && !dbt_args.iter().any( + |arg| matches!(arg, Value::Str(text) if DBT_VERBOSE_OPTIONS.contains(&text.as_str())), + ) + { + dbt_args.push(Value::Str(DBT_VERBOSE_OPTION.to_owned())); + namespace.set("dbt_args", Value::List(dbt_args)); + } + if CONCURRENCY_COMMANDS.contains(&command) { + let concurrency: Value = self + .concurrency(&namespace.value("concurrency")) + .map_err(|message| self.error(parser, &message))?; + namespace.set("concurrency", concurrency); + } + if CURSOR_COMMANDS.contains(&command) { + let untyped: bool = ["start_cursor", "end_cursor"] + .iter() + .any(|name| namespace.value(name) != Value::None); + let typed: bool = [ + "start_cursor_ts", + "end_cursor_ts", + "start_cursor_int", + "end_cursor_int", + ] + .iter() + .any(|name| namespace.value(name) != Value::None); + if untyped && typed { + return Err(self.error( + parser, + "--start-cursor/--end-cursor cannot be combined with the -ts or -int cursor flags", + )); + } + } + if command == SCOPE_COMMAND + && let Some(message) = scope_error(parser, &namespace) + { + return Err(self.error(parser, message)); + } + if command == TEST_COMMAND + && truthy(&namespace.value("inspect")) + && (truthy(&namespace.value("json")) || namespace.value("json_output") != Value::None) + { + return Err(self.error( + parser, + "test --inspect cannot be combined with --json or --json-output", + )); + } + if command == JANITOR_COMMAND + && namespace.value("as_target") != Value::None + && truthy(&namespace.value("auto_approve")) + { + return Err(self.error( + parser, + "janitor --as is inspection only and cannot be combined with --auto-approve", + )); + } + Ok(namespace) + } + + /// `resolve_env_default_concurrency`: the explicit value, else `SQLBUILD_CONCURRENCY`. + fn concurrency(&mut self, explicit: &Value) -> Result { + if *explicit != Value::None { + if integer(explicit) < 1 { + return Err("--concurrency must be >= 1".to_owned()); + } + return Ok(explicit.clone()); + } + let raw: String = match std::env::var_os(CONCURRENCY_ENV_VAR) { + None => return Ok(Value::None), + Some(raw) => raw.to_string_lossy().into_owned(), + }; + if raw.is_empty() { + return Ok(Value::None); + } + let Ok(value) = self.host.convert(INT_CONVERTER, &raw) else { + return Err(format!("{CONCURRENCY_ENV_VAR} must be an integer")); + }; + if integer(&value) < 1 { + return Err(format!("{CONCURRENCY_ENV_VAR} must be >= 1")); + } + Ok(value) + } +} + +/// `_validate_scope_args`: the first rule the scope options break. +fn scope_error(parser: &ParserSpec, namespace: &Namespace) -> Option<&'static str> { + let present = |name: &str| namespace.value(name) != Value::None; + let has_target: bool = present("scope_target"); + if has_target == present("scope_at") { + return Some("scope requires exactly one of TARGET or --at PATH"); + } + if present("scope_as_path") && !has_target { + return Some("scope --as-path requires TARGET"); + } + let dependency_depth: i128 = integer(&namespace.value("scope_dependency_depth")); + let page_size: i128 = integer(&namespace.value("scope_page_size")); + let browse: bool = present("scope_browse"); + if (browse || present("scope_list")) + && (present("scope_as_path") + || present("scope_explain") + || truthy(&namespace.value("scope_include_nearby")) + || dependency_depth != 0) + { + return Some( + "scope --browse/--list cannot be combined with --as-path, --explain, \ + --include-nearby, or --dependency-depth", + ); + } + if browse + && (present("scope_defined_under") + || truthy(&namespace.value("scope_kind")) + || present("scope_match") + || truthy(&namespace.value("scope_used_only")) + || namespace.value("scope_globals") != Value::Str(SCOPE_GLOBAL_SUMMARY.to_owned()) + || page_size != i128::from(SCOPE_DEFAULT_PAGE_SIZE)) + { + return Some( + "scope --browse is folder-only; use --list for declaration filters and pagination", + ); + } + if page_size < 1 { + return Some("scope --page-size must be positive"); + } + if integer(&namespace.value("scope_nearby_depth")) < 0 || dependency_depth < 0 { + return Some("scope depth values must be nonnegative"); + } + if let Some(explain) = text(&namespace.value("scope_explain")) { + let (prefix, qualified) = explain.split_once(':').unwrap_or((explain.as_str(), "")); + if !declaration_kinds(parser).contains(&prefix) || qualified.is_empty() { + return Some("scope --explain requires a declaration-qualified identity"); + } + } + None +} + +/// The `DeclarationKind` values: the choices of `sqb scope --kind`. +fn declaration_kinds(parser: &ParserSpec) -> Vec<&str> { + parser + .actions + .iter() + .flat_map(|action| &action.parsers) + .filter(|(name, _)| name == SCOPE_COMMAND) + .flat_map(|(_, scope)| &scope.actions) + .filter(|action| action.dest.as_deref() == Some(SCOPE_KIND_DEST)) + .flat_map(|action| action.choices.iter().flatten()) + .map(String::as_str) + .collect() +} + +fn text(value: &Value) -> Option { + match value { + Value::Str(text) => Some(text.clone()), + _ => None, + } +} + +fn list(value: &Value) -> Vec { + match value { + Value::List(items) => items.clone(), + _ => Vec::new(), + } +} + +/// Python truthiness of a parsed value. +fn truthy(value: &Value) -> bool { + match value { + Value::None | Value::EmptyDict => false, + Value::Bool(flag) => *flag, + Value::Int(text) => integer(&Value::Int(text.clone())) != 0, + Value::Str(text) => !text.is_empty(), + Value::List(items) => !items.is_empty(), + Value::Path(_) | Value::Host(_) => true, + } +} + +/// An integer value, saturated beyond `i128`. +fn integer(value: &Value) -> i128 { + let Value::Int(text) = value else { + return 0; + }; + text.parse::().unwrap_or(if text.starts_with('-') { + i128::MIN + } else { + i128::MAX + }) +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/parsing.rs b/crates/sqlbuild-cli/src/argv/_helpers/parsing.rs new file mode 100644 index 0000000000..7f7476b267 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/parsing.rs @@ -0,0 +1,568 @@ +//! `ArgumentParser.parse_known_args`, reproduced over the command table. + +use std::collections::{HashMap, HashSet}; + +use sqlbuild_core::text::main::is_decimal_digit::is_decimal_digit; + +use crate::argv::_helpers::formatting::{format_help, format_usage, format_version}; +use crate::argv::_helpers::patterns::match_groups; +use crate::argv::_helpers::values::{action_name, argument_error}; +use crate::argv::constants::{ + ERROR_PREFIX, ERROR_STRONG_STYLE, HELP_EXIT, OPTIONS_TERMINATOR, PARSER_ERROR_EXIT, + PREFIX_CHARS, STYLE_RESET, +}; +use crate::argv::models::{ + ActionKind, ActionSpec, ArgvSettings, Default, HelpText, Namespace, Nargs, ParserSpec, Stream, + Value, +}; +use crate::argv::types::ArgvHost; + +/// One parse: the caller's settings and host, and everything written so far. +pub(crate) struct Run<'a> { + pub(crate) settings: &'a ArgvSettings, + pub(crate) host: &'a mut dyn ArgvHost, + pub(crate) writes: Vec<(Stream, String)>, +} + +/// `SystemExit` with this status. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Exit(pub(crate) i64); + +/// What stops `_parse_known_args`: an `ArgumentError`'s text, or `SystemExit`. +pub(crate) enum Failure { + Argument(String), + Exit(Exit), +} + +impl From for Failure { + fn from(exit: Exit) -> Self { + Self::Exit(exit) + } +} + +/// One interpretation of an option string: action, option, separator and explicit argument. +#[derive(Debug, Clone)] +struct OptionTuple { + action: Option, + option_string: String, + separator: Option, + explicit: Option, +} + +/// A parsed namespace and the strings no action consumed. +pub(crate) type KnownArgs = (Namespace, Vec); + +impl Run<'_> { + /// `_print_message`: write non-empty text to `stream`. + pub(crate) fn write(&mut self, stream: Stream, text: String) { + if !text.is_empty() { + self.writes.push((stream, text)); + } + } + + /// `SqlbuildArgumentParser.error`: usage, then the coded message, to stderr; exit 2. + pub(crate) fn error(&mut self, parser: &ParserSpec, message: &str) -> Exit { + let usage: String = format_usage(self.settings, parser); + self.write(Stream::Stderr, usage); + let prefix: String = if self.settings.use_color { + format!("{ERROR_STRONG_STYLE}{ERROR_PREFIX}{STYLE_RESET}") + } else { + ERROR_PREFIX.to_owned() + }; + self.write( + Stream::Stderr, + format!("{}: {prefix} {message}\n\n", parser.prog), + ); + Exit(PARSER_ERROR_EXIT) + } + + /// `parse_known_args(args, namespace)`: the namespace and unrecognised strings, or the exit. + pub(crate) fn parse_known_args( + &mut self, + parser: &ParserSpec, + args: Vec, + mut namespace: Namespace, + ) -> Result { + for action in &parser.actions { + let (Some(dest), Default::Value(default)) = (&action.dest, &action.default) else { + continue; + }; + if !namespace.has(dest) { + namespace.set(dest, default.clone()); + } + } + let level: Level<'_, '_, '_> = Level::new(self, parser, args, namespace); + match level.parse() { + Ok((mut namespace, mut extras)) => { + if let Some(unrecognized) = namespace.unrecognized.take() { + extras.extend(unrecognized); + } + Ok((namespace, extras)) + } + Err(Failure::Argument(message)) => Err(self.error(parser, &message)), + Err(Failure::Exit(exit)) => Err(exit), + } + } +} + +/// The working state of one `_parse_known_args` call. +struct Level<'p, 'r, 'h> { + run: &'r mut Run<'h>, + parser: &'p ParserSpec, + namespace: Namespace, + arg_strings: Vec, + pattern: Vec, + option_indices: HashMap>, + positionals: Vec, + extras: Vec, + seen: HashSet, + seen_non_default: HashSet, +} + +impl<'p, 'r, 'h> Level<'p, 'r, 'h> { + fn new( + run: &'r mut Run<'h>, + parser: &'p ParserSpec, + arg_strings: Vec, + namespace: Namespace, + ) -> Self { + let mut pattern: Vec = Vec::with_capacity(arg_strings.len()); + let mut option_indices: HashMap> = HashMap::new(); + let mut terminated: bool = false; + for (index, arg) in arg_strings.iter().enumerate() { + if terminated { + pattern.push(b'A'); + } else if arg == OPTIONS_TERMINATOR { + pattern.push(b'-'); + terminated = true; + } else if let Some(tuples) = parse_optional(parser, arg) { + option_indices.insert(index, tuples); + pattern.push(b'O'); + } else { + pattern.push(b'A'); + } + } + Self { + run, + parser, + namespace, + arg_strings, + pattern, + option_indices, + positionals: parser + .actions + .iter() + .enumerate() + .filter(|(_, action)| action.option_strings.is_empty()) + .map(|(index, _)| index) + .collect(), + extras: Vec::new(), + seen: HashSet::new(), + seen_non_default: HashSet::new(), + } + } + + /// `_parse_known_args`: consume positionals and optionals alternately, then check requirements. + fn parse(mut self) -> Result { + let parser: &ParserSpec = self.parser; + let max_option: Option = self.option_indices.keys().copied().max(); + let mut start: usize = 0; + while max_option.is_some_and(|maximum| start <= maximum) { + let next: usize = (start..) + .find(|index| self.option_indices.contains_key(index)) + .unwrap_or(start); + if start != next { + let end: usize = self.consume_positionals(start)?; + if end > start { + start = end; + continue; + } + start = end; + } + if !self.option_indices.contains_key(&start) { + self.extras + .extend(self.arg_strings[start..next].iter().cloned()); + start = next; + } + start = self.consume_optional(start)?; + } + let stop: usize = self.consume_positionals(start)?; + self.extras.extend(self.arg_strings[stop..].iter().cloned()); + let required: Vec = parser + .actions + .iter() + .filter(|action| action.required && !self.seen.contains(&action.id)) + .map(|action| action_name(action).unwrap_or_default()) + .collect(); + if !required.is_empty() { + return Err(Failure::Argument(format!( + "the following arguments are required: {}", + required.join(", ") + ))); + } + for (group, required_group) in parser.mutex_groups.iter().enumerate() { + let members: Vec<&ActionSpec> = parser + .actions + .iter() + .filter(|action| action.mutex_group == Some(group)) + .collect(); + if *required_group + && !members + .iter() + .any(|action| self.seen_non_default.contains(&action.id)) + { + let names: Vec = members + .iter() + .filter(|action| action.help != HelpText::Suppress) + .filter_map(|action| action_name(action)) + .collect(); + return Err(Failure::Argument(format!( + "one of the arguments {} is required", + names.join(" ") + ))); + } + } + Ok((self.namespace, self.extras)) + } + + fn ignored_explicit(&self, action: &ActionSpec, explicit: &str) -> Failure { + Failure::Argument(argument_error( + action, + &format!("ignored explicit argument {}", self.run.host.repr(explicit)), + )) + } + + /// `consume_optional`: take the option at `start` and its arguments; the next index. + fn consume_optional(&mut self, start: usize) -> Result { + let parser: &ParserSpec = self.parser; + let tuples: Vec = self.option_indices[&start].clone(); + if tuples.len() > 1 { + let matches: Vec<&str> = tuples + .iter() + .map(|tuple| tuple.option_string.as_str()) + .collect(); + return Err(Failure::Argument(format!( + "ambiguous option: {} could match {}", + self.arg_strings[start], + matches.join(", ") + ))); + } + let OptionTuple { + mut action, + mut option_string, + mut separator, + mut explicit, + } = tuples[0].clone(); + let mut action_tuples: Vec<(usize, Vec)> = Vec::new(); + let stop: usize; + loop { + let Some(index) = action else { + self.extras.push(self.arg_strings[start].clone()); + return Ok(start + 1); + }; + let spec: &ActionSpec = &parser.actions[index]; + let Some(explicit_arg) = explicit.clone() else { + let first: usize = start + 1; + let count: usize = match_argument(spec, &self.pattern[first..])?; + stop = first + count; + action_tuples.push((index, self.arg_strings[first..stop].to_vec())); + break; + }; + let count: usize = match_argument(spec, b"A")?; + let second: char = option_string.chars().nth(1).unwrap_or_default(); + if count == 1 { + stop = start + 1; + action_tuples.push((index, vec![explicit_arg])); + break; + } + if count != 0 || second == PREFIX_CHARS || explicit_arg.is_empty() { + return Err(self.ignored_explicit(spec, &explicit_arg)); + } + if separator.as_deref().is_some_and(|sep| !sep.is_empty()) + || explicit_arg.starts_with(PREFIX_CHARS) + { + return Err(self.ignored_explicit(spec, &explicit_arg)); + } + action_tuples.push((index, Vec::new())); + let mut characters = explicit_arg.chars(); + let first: char = characters.next().unwrap_or_default(); + let rest: String = characters.collect(); + option_string = format!("{PREFIX_CHARS}{first}"); + let Some(next) = parser.option_index.get(&option_string) else { + self.extras.push(format!("{PREFIX_CHARS}{explicit_arg}")); + stop = start + 1; + break; + }; + action = Some(*next); + (separator, explicit) = if rest.is_empty() { + (None, None) + } else if let Some(value) = rest.strip_prefix('=') { + (Some("=".to_owned()), Some(value.to_owned())) + } else { + (Some(String::new()), Some(rest)) + }; + } + for (index, args) in action_tuples { + self.take_action(index, args)?; + } + Ok(stop) + } + + /// `consume_positionals`: match as many positionals as possible from `start`. + fn consume_positionals(&mut self, mut start: usize) -> Result { + let parser: &ParserSpec = self.parser; + let counts: Vec = match_partial(parser, &self.positionals, &self.pattern[start..]); + let consumed: Vec = self.positionals.drain(..counts.len()).collect(); + for (index, count) in consumed.into_iter().zip(counts) { + let action: &ActionSpec = &parser.actions[index]; + let mut args: Vec = self.arg_strings[start..start + count].to_vec(); + let strip: bool = if action.nargs == Nargs::Parser { + self.pattern.get(start) == Some(&b'-') + } else { + self.pattern[start..start + count].contains(&b'-') + }; + if strip && let Some(position) = args.iter().position(|arg| arg == OPTIONS_TERMINATOR) { + args.remove(position); + } + start += count; + self.take_action(index, args)?; + } + Ok(start) + } + + /// `take_action`: convert the strings, check mutual exclusion and call the action. + fn take_action(&mut self, index: usize, args: Vec) -> Result<(), Failure> { + let parser: &ParserSpec = self.parser; + let action: &ActionSpec = &parser.actions[index]; + self.seen.insert(action.id); + let values: Option = self + .run + .action_values(action, &args) + .map_err(Failure::Argument)?; + if !action.option_strings.is_empty() || !args.is_empty() { + self.seen_non_default.insert(action.id); + let conflict: Option<&ActionSpec> = action.mutex_group.and_then(|group| { + parser.actions.iter().find(|other| { + other.mutex_group == Some(group) + && other.id != action.id + && self.seen_non_default.contains(&other.id) + }) + }); + if let Some(other) = conflict { + return Err(Failure::Argument(argument_error( + action, + &format!( + "not allowed with argument {}", + action_name(other).unwrap_or_default() + ), + ))); + } + } + match values { + Some(values) => self.call_action(action, values), + None => Ok(()), + } + } + + /// `Action.__call__` for each action class the command table uses. + fn call_action(&mut self, action: &ActionSpec, values: Value) -> Result<(), Failure> { + let dest: &str = action.dest.as_deref().unwrap_or_default(); + match action.kind { + ActionKind::Store => self.namespace.set(dest, values), + ActionKind::StoreConst | ActionKind::StoreTrue | ActionKind::StoreFalse => { + self.namespace.set(dest, action.constant.clone()); + } + ActionKind::Append | ActionKind::Extend => { + let mut items: Vec = match self.namespace.get(dest) { + Some(Value::List(items)) => items.clone(), + _ => Vec::new(), + }; + match (action.kind, values) { + (ActionKind::Extend, Value::List(values)) => items.extend(values), + (_, value) => items.push(value), + } + self.namespace.set(dest, Value::List(items)); + } + ActionKind::Help => { + let help: String = format_help(self.run.settings, self.parser); + self.run.write(Stream::Stdout, help); + return Err(Failure::Exit(Exit(HELP_EXIT))); + } + ActionKind::Version => { + let version: String = self.run.host.program_version(); + let text: String = format_version(self.run.settings, action, &version); + self.run.write(Stream::Stdout, text); + return Err(Failure::Exit(Exit(HELP_EXIT))); + } + ActionKind::Parsers => self.call_parsers(action, values)?, + } + Ok(()) + } + + /// `_SubParsersAction.__call__`: parse the rest into a fresh namespace and copy it over. + fn call_parsers(&mut self, action: &ActionSpec, values: Value) -> Result<(), Failure> { + let Value::List(values) = values else { + return Ok(()); + }; + let strings: Vec = values + .into_iter() + .map(|value| match value { + Value::Str(text) => text, + _ => String::new(), + }) + .collect(); + let Some((name, rest)) = strings.split_first() else { + return Ok(()); + }; + if let Some(dest) = &action.dest { + self.namespace.set(dest, Value::Str(name.clone())); + } + let Some((_, subparser)) = action.parsers.iter().find(|(choice, _)| choice == name) else { + return Ok(()); + }; + let (subnamespace, leftover) = + self.run + .parse_known_args(subparser, rest.to_vec(), Namespace::default())?; + for (name, value) in subnamespace.entries { + self.namespace.set(&name, value); + } + if !leftover.is_empty() { + self.namespace + .unrecognized + .get_or_insert_with(Vec::new) + .extend(leftover); + } + Ok(()) + } +} + +/// `_match_argument`: how many strings an optional consumes, or its arity error. +fn match_argument(action: &ActionSpec, pattern: &[u8]) -> Result { + if let Some(counts) = match_groups(&[action], pattern) { + return Ok(counts[0]); + } + let message: &str = match action.nargs { + Nargs::Single => "expected one argument", + Nargs::Optional => "expected at most one argument", + Nargs::OneOrMore => "expected at least one argument", + _ => "expected 0 arguments", + }; + Err(Failure::Argument(argument_error(action, message))) +} + +/// `_match_arguments_partial`: argument counts for as many leading positionals as match. +fn match_partial(parser: &ParserSpec, positionals: &[usize], pattern: &[u8]) -> Vec { + for length in (1..=positionals.len()).rev() { + let actions: Vec<&ActionSpec> = positionals[..length] + .iter() + .map(|index| &parser.actions[*index]) + .collect(); + if let Some(mut counts) = match_groups(&actions, pattern) { + let end: usize = counts.iter().sum(); + if pattern.get(end) == Some(&b'O') { + while counts.last() == Some(&0) { + counts.pop(); + } + } + return counts; + } + } + Vec::new() +} + +/// `_parse_optional`: how a string starting with `-` reads as options, or `None` for an argument. +fn parse_optional(parser: &ParserSpec, arg: &str) -> Option> { + if !arg.starts_with(PREFIX_CHARS) { + return None; + } + if let Some(index) = parser.option_index.get(arg) { + return Some(vec![OptionTuple { + action: Some(*index), + option_string: arg.to_owned(), + separator: None, + explicit: None, + }]); + } + if arg.chars().count() == 1 { + return None; + } + if let Some((option, explicit)) = arg.split_once('=') + && let Some(index) = parser.option_index.get(option) + { + return Some(vec![OptionTuple { + action: Some(*index), + option_string: option.to_owned(), + separator: Some("=".to_owned()), + explicit: Some(explicit.to_owned()), + }]); + } + let tuples: Vec = option_tuples(parser, arg); + if !tuples.is_empty() { + return Some(tuples); + } + if looks_negative(arg) || arg.contains(' ') { + return None; + } + Some(vec![OptionTuple { + action: None, + option_string: arg.to_owned(), + separator: None, + explicit: None, + }]) +} + +/// `_get_option_tuples` with `allow_abbrev`: every option the string abbreviates. +fn option_tuples(parser: &ParserSpec, arg: &str) -> Vec { + let (prefix, separator, explicit) = match arg.split_once('=') { + Some((prefix, explicit)) => (prefix, Some("=".to_owned()), Some(explicit.to_owned())), + None => (arg, None, None), + }; + let second: char = arg.chars().nth(1).unwrap_or_default(); + let mut tuples: Vec = Vec::new(); + if second == PREFIX_CHARS { + for (option, index) in &parser.option_actions { + if option.starts_with(prefix) { + tuples.push(OptionTuple { + action: Some(*index), + option_string: option.clone(), + separator: separator.clone(), + explicit: explicit.clone(), + }); + } + } + return tuples; + } + let short_end: usize = arg.char_indices().nth(2).map_or(arg.len(), |(end, _)| end); + let (short_prefix, short_explicit) = arg.split_at(short_end); + for (option, index) in &parser.option_actions { + if option == short_prefix { + tuples.push(OptionTuple { + action: Some(*index), + option_string: option.clone(), + separator: Some(String::new()), + explicit: Some(short_explicit.to_owned()), + }); + } else if option.starts_with(prefix) { + tuples.push(OptionTuple { + action: Some(*index), + option_string: option.clone(), + separator: separator.clone(), + explicit: explicit.clone(), + }); + } + } + tuples +} + +/// `_negative_number_matcher.match` (`^-\d+$|^-\d*\.\d+$`): a negative-number-like string. +fn looks_negative(arg: &str) -> bool { + let rest: &str = &arg[1..]; + let body: &str = rest.strip_suffix('\n').unwrap_or(rest); + let integer: bool = !body.is_empty() && body.chars().all(is_decimal_digit); + let decimal: bool = body.split_once('.').is_some_and(|(whole, fraction)| { + whole.chars().all(is_decimal_digit) + && !fraction.is_empty() + && fraction.chars().all(is_decimal_digit) + }); + integer || decimal +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/patterns.rs b/crates/sqlbuild-cli/src/argv/_helpers/patterns.rs new file mode 100644 index 0000000000..59205784ea --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/patterns.rs @@ -0,0 +1,117 @@ +//! `argparse`'s nargs regular expressions over the `A`/`O`/`-` argument-string pattern. + +use std::collections::HashSet; + +use crate::argv::models::{ActionSpec, Nargs}; + +/// One greedy quantified character class: its characters and repetition bounds. +#[derive(Clone, Copy)] +struct Element { + characters: &'static str, + minimum: usize, + maximum: usize, +} + +const UNBOUNDED: usize = usize::MAX; + +const fn element(characters: &'static str, minimum: usize, maximum: usize) -> Element { + Element { + characters, + minimum, + maximum, + } +} + +/// `_get_nargs_pattern`: the capture group of one action, as greedy elements. +fn nargs_pattern(action: &ActionSpec) -> Vec { + let optional: bool = !action.option_strings.is_empty(); + match (action.nargs, optional) { + (Nargs::Single, true) => vec![element("A", 1, 1)], + (Nargs::Single, false) => vec![ + element("-", 0, UNBOUNDED), + element("A", 1, 1), + element("-", 0, UNBOUNDED), + ], + (Nargs::Optional, true) => vec![element("A", 0, 1)], + (Nargs::Optional, false) => vec![ + element("-", 0, UNBOUNDED), + element("A", 0, 1), + element("-", 0, UNBOUNDED), + ], + (Nargs::ZeroOrMore, true) => vec![element("A", 0, UNBOUNDED)], + (Nargs::ZeroOrMore, false) => { + vec![element("-", 0, UNBOUNDED), element("A-", 0, UNBOUNDED)] + } + (Nargs::OneOrMore, true) => vec![element("A", 1, UNBOUNDED)], + (Nargs::OneOrMore, false) => vec![ + element("-", 0, UNBOUNDED), + element("A", 1, 1), + element("A-", 0, UNBOUNDED), + ], + (Nargs::Parser, true) => vec![element("A", 1, 1), element("AO", 0, UNBOUNDED)], + (Nargs::Parser, false) => vec![ + element("-", 0, UNBOUNDED), + element("A", 1, 1), + element("-AO", 0, UNBOUNDED), + ], + (Nargs::Zero, true) => Vec::new(), + (Nargs::Zero, false) => vec![element("-", 0, UNBOUNDED)], + } +} + +/// `re.match` of the actions' concatenated groups at the start of `pattern`: each group's length. +pub(crate) fn match_groups(actions: &[&ActionSpec], pattern: &[u8]) -> Option> { + let mut elements: Vec<(usize, Element)> = Vec::new(); + for (group, action) in actions.iter().enumerate() { + elements.extend(nargs_pattern(action).into_iter().map(|item| (group, item))); + } + let mut matcher: Matcher<'_> = Matcher { + counts: vec![0; elements.len()], + elements, + pattern, + failed: HashSet::new(), + }; + if !matcher.backtrack(0, 0) { + return None; + } + let mut lengths: Vec = vec![0; actions.len()]; + for ((group, _), count) in matcher.elements.iter().zip(matcher.counts) { + lengths[*group] += count; + } + Some(lengths) +} + +/// A greedy backtracking match of quantified elements, remembering positions known to fail. +struct Matcher<'a> { + elements: Vec<(usize, Element)>, + pattern: &'a [u8], + counts: Vec, + failed: HashSet<(usize, usize)>, +} + +impl Matcher<'_> { + /// Python's greedy order: each element takes as many characters as it can first. + fn backtrack(&mut self, index: usize, position: usize) -> bool { + let Some((_, item)) = self.elements.get(index).copied() else { + return true; + }; + if self.failed.contains(&(index, position)) { + return false; + } + let available: usize = self.pattern[position..] + .iter() + .take_while(|character| item.characters.as_bytes().contains(character)) + .count() + .min(item.maximum); + if available >= item.minimum { + for count in (item.minimum..=available).rev() { + self.counts[index] = count; + if self.backtrack(index + 1, position + count) { + return true; + } + } + } + self.failed.insert((index, position)); + false + } +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/table.rs b/crates/sqlbuild-cli/src/argv/_helpers/table.rs new file mode 100644 index 0000000000..0783155dbe --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/table.rs @@ -0,0 +1,199 @@ +//! The embedded `sqb` command table, read once into parser specs. + +use std::collections::HashMap; +use std::sync::OnceLock; + +use serde_json::Value as Json; + +use crate::argv::constants::{COMMAND_TABLE, SUPPRESS_DEST}; +use crate::argv::models::{ + ActionKind, ActionSpec, ChoiceHelp, Default, GroupSpec, HelpText, Nargs, ParserSpec, Value, +}; + +static ROOT: OnceLock = OnceLock::new(); + +/// The root `sqb` parser. +pub(crate) fn root_parser() -> &'static ParserSpec { + ROOT.get_or_init(|| { + let table: Json = serde_json::from_str(COMMAND_TABLE).unwrap_or(Json::Null); + TableReader { next_id: 0 }.parser(&table) + }) +} + +/// Reads parser specs, numbering every action across the whole tree. +struct TableReader { + next_id: usize, +} + +impl TableReader { + fn parser(&mut self, json: &Json) -> ParserSpec { + let groups: Vec = items(json, "groups") + .iter() + .map(|group| GroupSpec { + title: text(group, "title"), + description: text(group, "description"), + }) + .collect(); + let mutex_groups: Vec = items(json, "mutex_groups") + .iter() + .map(|required| required.as_bool().unwrap_or(false)) + .collect(); + let actions: Vec = items(json, "actions") + .iter() + .map(|item| self.action(item)) + .collect(); + let mut option_actions: Vec<(String, usize)> = Vec::new(); + let mut option_index: HashMap = HashMap::new(); + for (index, action) in actions.iter().enumerate() { + for option in &action.option_strings { + option_actions.push((option.clone(), index)); + option_index.insert(option.clone(), index); + } + } + ParserSpec { + prog: text(json, "prog").unwrap_or_default(), + usage: text(json, "usage"), + description: text(json, "description"), + epilog: text(json, "epilog"), + groups, + mutex_groups, + actions, + option_actions, + option_index, + } + } + + fn action(&mut self, json: &Json) -> ActionSpec { + let id: usize = self.next_id; + self.next_id += 1; + let dest: String = text(json, "dest").unwrap_or_default(); + let parsers: Vec<(String, ParserSpec)> = items(json, "parsers") + .iter() + .map(|item| { + ( + text(item, "name").unwrap_or_default(), + self.parser(&item["parser"]), + ) + }) + .collect(); + let choices: Option> = if parsers.is_empty() { + json.get("choices") + .and_then(Json::as_array) + .map(|choices| choices.iter().map(plain_text).collect()) + } else { + Some(parsers.iter().map(|(name, _)| name.clone()).collect()) + }; + ActionSpec { + id, + kind: kind(json), + option_strings: items(json, "options").iter().map(plain_text).collect(), + dest: (dest != SUPPRESS_DEST).then_some(dest), + nargs: nargs(json.get("nargs").unwrap_or(&Json::Null)), + constant: value(json.get("const").unwrap_or(&Json::Null)), + default: default(json.get("default").unwrap_or(&Json::Null)), + converter: text(json, "type"), + choices, + required: json + .get("required") + .and_then(Json::as_bool) + .unwrap_or(false), + help: help(json.get("help").unwrap_or(&Json::Null)), + metavar: metavar(json.get("metavar").unwrap_or(&Json::Null)), + group: json.get("group").and_then(Json::as_u64).unwrap_or(0) as usize, + mutex_group: json + .get("mutex_group") + .and_then(Json::as_u64) + .map(|group| group as usize), + version: text(json, "version"), + parsers, + choices_help: items(json, "choices_help") + .iter() + .map(|choice| ChoiceHelp { + metavar: text(choice, "metavar").unwrap_or_default(), + help: help(choice.get("help").unwrap_or(&Json::Null)), + }) + .collect(), + } + } +} + +fn kind(json: &Json) -> ActionKind { + match json.get("kind").and_then(Json::as_str).unwrap_or_default() { + "store_true" => ActionKind::StoreTrue, + "store_false" => ActionKind::StoreFalse, + "store_const" => ActionKind::StoreConst, + "append" => ActionKind::Append, + "extend" => ActionKind::Extend, + "help" => ActionKind::Help, + "version" => ActionKind::Version, + "parsers" => ActionKind::Parsers, + _ => ActionKind::Store, + } +} + +fn nargs(json: &Json) -> Nargs { + match json { + Json::Number(_) => Nargs::Zero, + Json::String(text) => match text.as_str() { + "?" => Nargs::Optional, + "*" => Nargs::ZeroOrMore, + "+" => Nargs::OneOrMore, + _ => Nargs::Parser, + }, + _ => Nargs::Single, + } +} + +fn suppressed(json: &Json) -> bool { + json.get("suppress") + .and_then(Json::as_bool) + .unwrap_or(false) +} + +fn default(json: &Json) -> Default { + if suppressed(json) { + return Default::Suppress; + } + Default::Value(value(json)) +} + +fn help(json: &Json) -> HelpText { + match json { + Json::String(text) => HelpText::Text(text.clone()), + Json::Null => HelpText::None, + _ => HelpText::Suppress, + } +} + +fn metavar(json: &Json) -> Option> { + match json { + Json::String(text) => Some(Some(text.clone())), + Json::Null => None, + _ => Some(None), + } +} + +fn value(json: &Json) -> Value { + match json { + Json::Bool(flag) => Value::Bool(*flag), + Json::Number(number) => Value::Int(number.to_string()), + Json::String(text) => Value::Str(text.clone()), + Json::Array(items) => Value::List(items.iter().map(value).collect()), + Json::Object(_) => Value::EmptyDict, + Json::Null => Value::None, + } +} + +fn items<'a>(json: &'a Json, key: &str) -> &'a [Json] { + json.get(key) + .and_then(Json::as_array) + .map_or(&[], Vec::as_slice) +} + +fn text(json: &Json, key: &str) -> Option { + json.get(key).and_then(Json::as_str).map(str::to_owned) +} + +fn plain_text(json: &Json) -> String { + json.as_str().unwrap_or_default().to_owned() +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/text_wrap.rs b/crates/sqlbuild-cli/src/argv/_helpers/text_wrap.rs new file mode 100644 index 0000000000..552185d67d --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/text_wrap.rs @@ -0,0 +1,186 @@ +//! Python's `textwrap.wrap` with its default options, as `argparse` uses it. + +use sqlbuild_core::text::main::is_decimal_digit::is_decimal_digit; +use sqlbuild_core::text::main::python_isalnum::python_isalnum; + +use crate::argv::constants::MINIMUM_EM_DASH; + +/// `_whitespace_matcher.sub(' ', text).strip()`: ASCII whitespace runs become one space. +pub(crate) fn normalize_whitespace(text: &str) -> String { + let mut normalized: String = String::with_capacity(text.len()); + let mut in_space: bool = false; + for character in text.chars() { + if matches!(character, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c') { + in_space = true; + continue; + } + if in_space && !normalized.is_empty() { + normalized.push(' '); + } + in_space = false; + normalized.push(character); + } + normalized +} + +/// `textwrap.wrap(text, width, initial_indent=indent, subsequent_indent=indent)`. +pub(crate) fn wrap(text: &str, width: usize, indent: &str) -> Vec { + let characters: Vec = text.chars().collect(); + let mut chunks: Vec> = split_chunks(&characters); + chunks.reverse(); + let indent_length: usize = indent.chars().count(); + let mut lines: Vec = Vec::new(); + while !chunks.is_empty() { + let mut line: Vec> = Vec::new(); + let mut length: usize = 0; + let available: i64 = width as i64 - indent_length as i64; + if !lines.is_empty() && chunks.last().is_some_and(|chunk| is_blank(chunk)) { + chunks.pop(); + } + while let Some(chunk) = chunks.last() { + if (length + chunk.len()) as i64 <= available { + length += chunk.len(); + line.push(chunks.pop().unwrap_or_default()); + } else { + break; + } + } + if let Some(chunk) = chunks.pop_if(|chunk| chunk.len() as i64 > available) { + let (head, rest) = split_long_word(chunk, line.is_empty(), length, available); + line.extend(head); + chunks.extend(rest); + } + if line.last().is_some_and(|chunk| is_blank(chunk)) { + line.pop(); + } + if !line.is_empty() { + let joined: String = line.iter().flatten().collect(); + lines.push(format!("{indent}{joined}")); + } + } + lines +} + +/// `_handle_long_word`: the part of a too-long chunk the line takes, and what stays queued. +fn split_long_word( + mut chunk: Vec, + line_is_empty: bool, + length: usize, + width: i64, +) -> (Option>, Option>) { + let space_left: i64 = if width < 1 { 1 } else { width - length as i64 }; + if space_left <= 0 { + return if line_is_empty { + (Some(chunk), None) + } else { + (None, Some(chunk)) + }; + } + let space_left: usize = space_left as usize; + let mut end: usize = space_left.min(chunk.len()); + if chunk.len() > space_left + && let Some(hyphen) = chunk[..space_left] + .iter() + .rposition(|character| *character == '-') + && hyphen > 0 + && chunk[..hyphen].iter().any(|character| *character != '-') + { + end = hyphen + 1; + } + let rest: Vec = chunk.split_off(end); + (Some(chunk), Some(rest)) +} + +fn is_blank(chunk: &[char]) -> bool { + chunk.iter().all(|character| character.is_whitespace()) +} + +fn is_space(character: char) -> bool { + matches!(character, '\t' | '\n' | '\x0b' | '\x0c' | '\r' | ' ') +} + +fn is_word(character: char) -> bool { + character == '_' || python_isalnum(character) +} + +fn is_word_punct(character: char) -> bool { + is_word(character) || matches!(character, '!' | '"' | '\'' | '&' | '.' | ',' | '?') +} + +fn is_letter(character: char) -> bool { + is_word(character) && !is_decimal_digit(character) +} + +/// `TextWrapper.wordsep_re.split`: whitespace runs, em-dashes and possibly hyphenated words. +fn split_chunks(text: &[char]) -> Vec> { + let mut chunks: Vec> = Vec::new(); + let mut start: usize = 0; + while start < text.len() { + let end: usize = chunk_end(text, start); + chunks.push(text[start..end].to_vec()); + start = end; + } + chunks +} + +fn at(text: &[char], index: usize) -> Option { + text.get(index).copied() +} + +fn letter_at(text: &[char], index: Option) -> bool { + index + .and_then(|index| at(text, index)) + .is_some_and(is_letter) +} + +/// The end of a `-{2,}(?=\w)` match at `start`, if there is one. +fn em_dash_end(text: &[char], start: usize) -> Option { + let count: usize = text[start..] + .iter() + .take_while(|character| **character == '-') + .count(); + (count >= MINIMUM_EM_DASH && at(text, start + count).is_some_and(is_word)) + .then_some(start + count) +} + +fn chunk_end(text: &[char], start: usize) -> usize { + if is_space(text[start]) { + return start + + text[start..] + .iter() + .take_while(|character| is_space(**character)) + .count(); + } + if start > 0 + && is_word_punct(text[start - 1]) + && let Some(end) = em_dash_end(text, start) + { + return end; + } + let mut position: usize = start + 1; + loop { + if at(text, position) == Some('-') { + let two_letters: bool = letter_at(text, position.checked_sub(2)) + && letter_at(text, position.checked_sub(1)); + let letter_hyphen_letter: bool = letter_at(text, position.checked_sub(3)) + && at(text, position.wrapping_sub(2)) == Some('-') + && letter_at(text, position.checked_sub(1)); + let ahead: bool = letter_at(text, Some(position + 1)) + && (letter_at(text, Some(position + 2)) + || (at(text, position + 2) == Some('-') + && letter_at(text, Some(position + 3)))); + if (two_letters || letter_hyphen_letter) && ahead { + return position + 1; + } + } + match at(text, position) { + None => return position, + Some(character) if is_space(character) => return position, + Some(_) => {} + } + if is_word_punct(text[position - 1]) && em_dash_end(text, position).is_some() { + return position; + } + position += 1; + } +} diff --git a/crates/sqlbuild-cli/src/argv/_helpers/values.rs b/crates/sqlbuild-cli/src/argv/_helpers/values.rs new file mode 100644 index 0000000000..df7da8834b --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/_helpers/values.rs @@ -0,0 +1,130 @@ +//! `_get_values`, `_get_value` and `_check_value`, and how `ArgumentError` names an action. + +use sqlbuild_core::text::main::python_str_repr::python_str_repr; + +use crate::argv::_helpers::parsing::Run; +use crate::argv::constants::PATH_CONVERTER; +use crate::argv::models::{ActionSpec, Default, Nargs, TypeFailure, Value}; + +/// `_get_action_name`: how errors name an action, if it has a name. +pub(crate) fn action_name(action: &ActionSpec) -> Option { + if !action.option_strings.is_empty() { + return Some(action.option_strings.join("/")); + } + if let Some(Some(metavar)) = &action.metavar { + return Some(metavar.clone()); + } + if let Some(dest) = &action.dest { + return Some(dest.clone()); + } + action + .choices + .as_ref() + .filter(|choices| !choices.is_empty()) + .map(|choices| format!("{{{}}}", choices.join(","))) +} + +/// `str(ArgumentError(action, message))`. +pub(crate) fn argument_error(action: &ActionSpec, message: &str) -> String { + match action_name(action) { + Some(name) => format!("argument {name}: {message}"), + None => message.to_owned(), + } +} + +impl Run<'_> { + /// `_get_values`: the converted value an action is called with, or `None` for `SUPPRESS`. + pub(crate) fn action_values( + &mut self, + action: &ActionSpec, + args: &[String], + ) -> Result, String> { + let positional: bool = action.option_strings.is_empty(); + if args.is_empty() && action.nargs == Nargs::Optional { + let value: Value = if positional { + match &action.default { + Default::Suppress => return Ok(None), + Default::Value(value) => value.clone(), + } + } else { + action.constant.clone() + }; + return match value { + Value::Str(text) => self.converted_value(action, &text).map(Some), + value => Ok(Some(value)), + }; + } + if args.is_empty() && action.nargs == Nargs::ZeroOrMore && positional { + return Ok(match &action.default { + Default::Suppress => None, + Default::Value(Value::None) => Some(Value::List(Vec::new())), + Default::Value(value) => Some(value.clone()), + }); + } + if args.len() == 1 && matches!(action.nargs, Nargs::Single | Nargs::Optional) { + let value: Value = self.converted_value(action, &args[0])?; + self.check_value(action, &value)?; + return Ok(Some(value)); + } + let mut values: Vec = Vec::with_capacity(args.len()); + for arg in args { + values.push(self.converted_value(action, arg)?); + } + let checked: &[Value] = if action.nargs == Nargs::Parser { + values.get(..1).unwrap_or_default() + } else { + &values + }; + for value in checked { + self.check_value(action, value)?; + } + Ok(Some(Value::List(values))) + } + + /// `_get_value`: apply the action's `type=` converter. + pub(crate) fn converted_value( + &mut self, + action: &ActionSpec, + text: &str, + ) -> Result { + let Some(converter) = action.converter.as_deref() else { + return Ok(Value::Str(text.to_owned())); + }; + if converter == PATH_CONVERTER { + return Ok(Value::Path(text.to_owned())); + } + match self.host.convert(converter, text) { + Ok(value) => Ok(value), + Err(TypeFailure::ArgumentType(message)) => Err(argument_error(action, &message)), + Err(TypeFailure::Invalid) => Err(argument_error( + action, + &format!("invalid {converter} value: {}", self.host.repr(text)), + )), + } + } + + /// `_check_value`: a converted value must be one of the action's choices. + fn check_value(&self, action: &ActionSpec, value: &Value) -> Result<(), String> { + let Some(choices) = &action.choices else { + return Ok(()); + }; + let Value::Str(text) = value else { + return Ok(()); + }; + if choices.iter().any(|choice| choice == text) { + return Ok(()); + } + let listed: Vec = choices + .iter() + .map(|choice| python_str_repr(choice)) + .collect(); + Err(argument_error( + action, + &format!( + "invalid choice: {} (choose from {})", + self.host.repr(text), + listed.join(", ") + ), + )) + } +} diff --git a/crates/sqlbuild-cli/src/argv/command_table.json b/crates/sqlbuild-cli/src/argv/command_table.json new file mode 100644 index 0000000000..86e9ecdfb6 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/command_table.json @@ -0,0 +1,6716 @@ +{ + "prog": "sqb", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "version", + "options": [ + "--version", + "-V" + ], + "dest": "version", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show program's version number and exit", + "group": 1, + "version": "sqb {version}" + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": false, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": false, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + }, + { + "kind": "parsers", + "dest": "command", + "nargs": "A...", + "group": 0, + "parsers": [ + { + "name": "compile", + "parser": { + "prog": "sqb compile", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-cache" + ], + "dest": "no_cache", + "nargs": 0, + "const": true, + "default": false, + "help": "Bypass the compile analysis cache for this invocation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-to" + ], + "dest": "defer_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--manifest" + ], + "dest": "manifest", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dag" + ], + "dest": "dag", + "nargs": "?", + "const": "", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--lineage-mode" + ], + "dest": "compile_lineage_mode", + "default": "fast", + "choices": [ + "fast", + "rich", + "none" + ], + "help": "Column lineage mode: fast (default), rich (slower), or none", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--profile-skip-discovery-sql-analysis" + ], + "dest": "profile_skip_discovery_sql_analysis", + "nargs": 0, + "const": true, + "default": false, + "help": "Diagnostic: skip SQL analysis-assisted discovery parsing", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--profile-skip-column-inference" + ], + "dest": "profile_skip_column_inference", + "nargs": 0, + "const": true, + "default": false, + "help": "Diagnostic: skip compile-time column inference", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--profile-skip-contracts" + ], + "dest": "profile_skip_contracts", + "nargs": 0, + "const": true, + "default": false, + "help": "Diagnostic: skip offline contract validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--profile-skip-write" + ], + "dest": "profile_skip_write", + "nargs": 0, + "const": true, + "default": false, + "help": "Diagnostic: skip writing target/compiled artifacts", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "dag", + "parser": { + "prog": "sqb dag", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "plan", + "parser": { + "prog": "sqb plan", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-cache" + ], + "dest": "no_cache", + "nargs": 0, + "const": true, + "default": false, + "help": "Bypass the compile analysis cache for this invocation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-to" + ], + "dest": "defer_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-sources-to" + ], + "dest": "defer_sources_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--as" + ], + "dest": "as_target", + "help": "Preview the plan as another configured target through the active target's connection; inspection only", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--full-refresh" + ], + "dest": "full_refresh", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--selection-diagnostics" + ], + "dest": "selection_diagnostics", + "nargs": 0, + "const": true, + "default": false, + "help": "Include direct-mode stale-out-of-selection diagnostics.", + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-python" + ], + "dest": "include_python", + "nargs": 0, + "const": false, + "default": true, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--verbose", + "-v" + ], + "dest": "verbose", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--load" + ], + "dest": "load_sources", + "nargs": 0, + "const": true, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-load" + ], + "dest": "load_sources", + "nargs": 0, + "const": false, + "default": true, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor" + ], + "dest": "start_cursor", + "help": "override the start cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor" + ], + "dest": "end_cursor", + "help": "override the end cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-ts" + ], + "dest": "start_cursor_ts", + "help": "timestamp start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-ts" + ], + "dest": "end_cursor_ts", + "help": "timestamp end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-int" + ], + "dest": "start_cursor_int", + "help": "integer start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-int" + ], + "dest": "end_cursor_int", + "help": "integer end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-microbatches" + ], + "dest": "max_microbatches", + "type": "_positive_integer", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "build", + "parser": { + "prog": "sqb build", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-cache" + ], + "dest": "no_cache", + "nargs": 0, + "const": true, + "default": false, + "help": "Bypass the compile analysis cache for this invocation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-to" + ], + "dest": "defer_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-clone-from" + ], + "dest": "defer_clone_from", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-sources-to" + ], + "dest": "defer_sources_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--selection-diagnostics" + ], + "dest": "selection_diagnostics", + "nargs": 0, + "const": true, + "default": false, + "help": "Include direct-mode stale-out-of-selection diagnostics.", + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-python" + ], + "dest": "include_python", + "nargs": 0, + "const": false, + "default": true, + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-tests" + ], + "dest": "run_tests", + "nargs": 0, + "const": false, + "default": true, + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-audits" + ], + "dest": "run_audits", + "nargs": 0, + "const": false, + "default": true, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--manifest" + ], + "dest": "manifest", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--event-output" + ], + "dest": "event_output", + "type": "Path", + "help": { + "suppress": true + }, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor" + ], + "dest": "start_cursor", + "help": "override the start cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor" + ], + "dest": "end_cursor", + "help": "override the end cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-ts" + ], + "dest": "start_cursor_ts", + "help": "timestamp start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-ts" + ], + "dest": "end_cursor_ts", + "help": "timestamp end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-int" + ], + "dest": "start_cursor_int", + "help": "integer start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-int" + ], + "dest": "end_cursor_int", + "help": "integer end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-microbatches" + ], + "dest": "max_microbatches", + "type": "_positive_integer", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--load" + ], + "dest": "load_sources", + "nargs": 0, + "const": true, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_false", + "options": [ + "--no-load" + ], + "dest": "load_sources", + "nargs": 0, + "const": false, + "default": true, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--reload" + ], + "dest": "reload", + "nargs": 0, + "const": true, + "default": false, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--fail-fast" + ], + "dest": "fail_fast", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--full-refresh" + ], + "dest": "full_refresh", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--allow-snapshot-full-refresh" + ], + "dest": "allow_snapshot_full_refresh", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--allow-table-type-downgrade" + ], + "dest": "allow_table_type_downgrade", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--allow-retention-decrease" + ], + "dest": "allow_retention_decrease", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--allow-missing-migration-origin" + ], + "dest": "allow_missing_migration_origin", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--allow-snapshot-schema-change" + ], + "dest": "allow_snapshot_schema_change", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--verbose", + "-v" + ], + "dest": "verbose", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "freshness", + "parser": { + "prog": "sqb freshness", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--state" + ], + "dest": "state", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--fail-on-error" + ], + "dest": "fail_on_error", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--fail-on-stale" + ], + "dest": "fail_on_stale", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "test", + "parser": { + "prog": "sqb test", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--case" + ], + "dest": "case", + "help": "Run one named parameter case", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--inspect" + ], + "dest": "inspect", + "nargs": 0, + "const": true, + "default": false, + "help": "Show the resolved test plan", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "check", + "parser": { + "prog": "sqb check", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "audit", + "parser": { + "prog": "sqb audit", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defer-to" + ], + "dest": "defer_to", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "format", + "parser": { + "prog": "sqb format", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--check" + ], + "dest": "format_check", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--diff" + ], + "dest": "format_diff", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--fix" + ], + "dest": "format_fix", + "nargs": 0, + "const": true, + "default": false, + "help": "apply compiler-verified meaning-preserving Rule fixes", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--fixtures-only" + ], + "dest": "format_fixtures_only", + "nargs": 0, + "const": true, + "default": false, + "help": "only remove redundant typed-null SQL test fixture columns", + "group": 1 + }, + { + "kind": "store", + "dest": "format_paths", + "nargs": "*", + "default": [], + "help": "SQL files or folders to format, relative to the current directory", + "metavar": "PATH", + "group": 0 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "load", + "parser": { + "prog": "sqb load", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--reload" + ], + "dest": "reload", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor" + ], + "dest": "start_cursor", + "help": "override the start cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor" + ], + "dest": "end_cursor", + "help": "override the end cursor, typed from the selected models' cursor type", + "metavar": "VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-ts" + ], + "dest": "start_cursor_ts", + "help": "timestamp start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-ts" + ], + "dest": "end_cursor_ts", + "help": "timestamp end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--start-cursor-int" + ], + "dest": "start_cursor_int", + "help": "integer start cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--end-cursor-int" + ], + "dest": "end_cursor_int", + "help": "integer end cursor override", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "seed", + "parser": { + "prog": "sqb seed", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "dest": "positional_select", + "nargs": "*", + "default": [], + "help": "selectors to include, the same as --select; combined with any --select values", + "metavar": "SELECTOR", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "clone", + "parser": { + "prog": "sqb clone", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--from" + ], + "dest": "from_target", + "required": true, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--to" + ], + "dest": "to_target", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--hard-copy" + ], + "dest": "hard_copy", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--verbose", + "-v" + ], + "dest": "verbose", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dbt-target-path" + ], + "dest": "dbt_target_path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--event-output" + ], + "dest": "event_output", + "type": "Path", + "help": { + "suppress": true + }, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "diff", + "parser": { + "prog": "sqb diff", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "target_range", + "nargs": "?", + "metavar": "FROM[:TO]", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--full" + ], + "dest": "full", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--schema-only" + ], + "dest": "schema_only", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--bounded" + ], + "dest": "bounded", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--verbose", + "-v" + ], + "dest": "verbose", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-column-examples" + ], + "dest": "max_column_examples", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-row-only-examples" + ], + "dest": "max_row_only_examples", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--sample-rows" + ], + "dest": "sample_rows", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--sample-seed" + ], + "dest": "sample_seed", + "type": "int", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--exhaustive" + ], + "dest": "exhaustive", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-models" + ], + "dest": "max_models", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-columns" + ], + "dest": "max_columns", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "help": "target connection for raw-query diff (defaults to the active target)", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--left-query" + ], + "dest": "left_query", + "help": "literal left-side SQL query", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--left-query-file" + ], + "dest": "left_query_file", + "type": "Path", + "help": "path to left-side SQL query", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--right-query" + ], + "dest": "right_query", + "help": "literal right-side SQL query", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--right-query-file" + ], + "dest": "right_query_file", + "type": "Path", + "help": "path to right-side SQL query", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--left-label" + ], + "dest": "left_label", + "help": "display label for the left query", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--right-label" + ], + "dest": "right_label", + "help": "display label for the right query", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--key" + ], + "dest": "key", + "nargs": "+", + "default": [], + "help": "row identity columns; several values or repeated flags form a composite key", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--unkeyed" + ], + "dest": "unkeyed", + "nargs": 0, + "const": true, + "default": false, + "help": "compare exact full-row multiplicities without a key", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude-column" + ], + "dest": "exclude_column", + "nargs": "+", + "default": [], + "help": "columns to omit from row comparison; takes several values or repeated flags", + "group": 1 + }, + { + "kind": "append", + "options": [ + "--tolerance" + ], + "dest": "tolerance", + "default": [], + "help": "numeric tolerance as COLUMN:absolute=VALUE or COLUMN:relative=VALUE", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-value-length" + ], + "dest": "max_value_length", + "type": "int", + "help": "maximum rendered characters per example value", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-example-values" + ], + "dest": "no_example_values", + "nargs": 0, + "const": true, + "default": false, + "help": "show example keys and counts without values", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--full-example-values" + ], + "dest": "full_example_values", + "nargs": 0, + "const": true, + "default": false, + "help": "explicitly render complete example values", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "help": "write structured diff output to stdout", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "scope", + "parser": { + "prog": "sqb scope", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "scope_target", + "nargs": "?", + "metavar": "TARGET", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--at" + ], + "dest": "scope_at", + "metavar": "PATH", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--as-path" + ], + "dest": "scope_as_path", + "metavar": "PATH", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--browse" + ], + "dest": "scope_browse", + "metavar": "PATH", + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--list" + ], + "dest": "scope_list", + "metavar": "PATH", + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--defined-under" + ], + "dest": "scope_defined_under", + "group": 1 + }, + { + "kind": "append", + "options": [ + "--kind" + ], + "dest": "scope_kind", + "default": [], + "choices": [ + "macro", + "enum", + "constant", + "audit", + "singular_audit", + "schema", + "sql_hook", + "python_hook" + ], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--match" + ], + "dest": "scope_match", + "metavar": "GLOB", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--used-only" + ], + "dest": "scope_used_only", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--include-nearby" + ], + "dest": "scope_include_nearby", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--nearby-depth" + ], + "dest": "scope_nearby_depth", + "default": 1, + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--dependency-depth" + ], + "dest": "scope_dependency_depth", + "default": 0, + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--explain" + ], + "dest": "scope_explain", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--globals" + ], + "dest": "scope_globals", + "default": "summary", + "choices": [ + "summary", + "used", + "all" + ], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--page-size" + ], + "dest": "scope_page_size", + "default": 100, + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--after" + ], + "dest": "scope_after", + "metavar": "CURSOR", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--paths" + ], + "dest": "scope_paths", + "default": "relative", + "choices": [ + "relative", + "compact", + "none" + ], + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-cache" + ], + "dest": "no_cache", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--verbose", + "-v" + ], + "dest": "verbose", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "debug", + "parser": { + "prog": "sqb debug", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-connection" + ], + "dest": "no_connection", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "cost", + "parser": { + "prog": "sqb cost", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "cost_selector", + "nargs": "?", + "default": "latest", + "metavar": "latest|history|run_id", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--limit" + ], + "dest": "cost_limit", + "type": "int", + "help": "limit returned rows", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-limit" + ], + "dest": "cost_no_limit", + "nargs": 0, + "const": true, + "default": false, + "help": "return all matching rows", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--sort" + ], + "dest": "cost_sort", + "help": "metric used to sort results", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--order" + ], + "dest": "cost_order", + "choices": [ + "asc", + "desc" + ], + "help": "sort direction", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--since" + ], + "dest": "cost_since", + "help": "inclusive ISO date/datetime or relative duration such as 7d", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--until" + ], + "dest": "cost_until", + "help": "inclusive ISO date or timezone-aware datetime", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "help": "write stable JSON to stdout", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "query", + "parser": { + "prog": "sqb query", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "query_sql", + "nargs": "?", + "metavar": "sql", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--file" + ], + "dest": "query_file", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--format" + ], + "dest": "query_format", + "default": "long", + "choices": [ + "long", + "table", + "json", + "csv" + ], + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_const", + "options": [ + "--json" + ], + "dest": "query_format", + "nargs": 0, + "const": "json", + "help": "shorthand for --format json", + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--limit" + ], + "dest": "query_limit", + "default": 20, + "type": "int", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-limit" + ], + "dest": "query_no_limit", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "lineage", + "parser": { + "prog": "sqb lineage", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "lineage_targets", + "nargs": "*", + "default": [], + "help": "models, sources or seeds (optionally kind-prefixed, e.g. model:orders), or one model.column", + "metavar": "target", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--no-sql-analysis", + "--no-sql-validation" + ], + "dest": "no_sql_validation", + "nargs": 0, + "const": true, + "default": false, + "help": "disable SQL syntax, binding, type inference, and semantic validation", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--format" + ], + "dest": "lineage_format", + "default": "tree", + "choices": [ + "tree", + "json", + "list" + ], + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_const", + "options": [ + "--json" + ], + "dest": "lineage_format", + "nargs": 0, + "const": "json", + "help": "shorthand for --format json", + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--include-uses" + ], + "dest": "lineage_include_uses", + "nargs": 0, + "const": true, + "default": false, + "help": "include direct filter, join, grouping, window, and ordering column uses", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--direction" + ], + "dest": "lineage_direction", + "choices": [ + "upstream", + "downstream", + "both" + ], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--depth" + ], + "dest": "lineage_depth", + "default": "all", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--mode" + ], + "dest": "lineage_mode", + "default": "rich", + "choices": [ + "rich", + "fast" + ], + "help": "Column lineage mode: rich (default) or fast", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "contract", + "parser": { + "prog": "sqb contract", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "parsers", + "dest": "contract_command", + "nargs": "A...", + "required": true, + "group": 0, + "parsers": [ + { + "name": "diff", + "parser": { + "prog": "sqb contract diff", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--from" + ], + "dest": "contract_from", + "required": true, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "generate", + "parser": { + "prog": "sqb contract generate", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--from" + ], + "dest": "contract_from", + "required": true, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--vars" + ], + "dest": "vars", + "default": { + "dict": {} + }, + "type": "parse_cli_vars", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--write" + ], + "dest": "contract_write", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--overwrite" + ], + "dest": "overwrite", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + } + ], + "choices_help": [] + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "rename", + "parser": { + "prog": "sqb rename", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "refactor_target", + "required": true, + "help": " or .; the model: and column: prefixes are optional", + "metavar": "TARGET", + "group": 0 + }, + { + "kind": "store", + "dest": "refactor_new_name", + "required": true, + "metavar": "NEW_NAME", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--cascade" + ], + "dest": "refactor_cascade", + "nargs": 0, + "const": true, + "default": false, + "help": "rename a column through every downstream model that passes it through", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--dry-run" + ], + "dest": "dry_run", + "nargs": 0, + "const": true, + "default": false, + "help": "print every edit and verify it compiles, without writing files", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "help": "print the result as JSON on stdout; progress goes to stderr", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "mv", + "parser": { + "prog": "sqb mv", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "refactor_target", + "required": true, + "help": "; the model: prefix is optional", + "metavar": "TARGET", + "group": 0 + }, + { + "kind": "store", + "dest": "refactor_destination", + "required": true, + "help": "new .sql path or folder/", + "metavar": "DESTINATION", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--dry-run" + ], + "dest": "dry_run", + "nargs": 0, + "const": true, + "default": false, + "help": "print every edit and verify it compiles, without writing files", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "help": "print the result as JSON on stdout; progress goes to stderr", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "clean", + "parser": { + "prog": "sqb clean", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "janitor", + "parser": { + "prog": "sqb janitor", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--auto-approve" + ], + "dest": "auto_approve", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--retention-days" + ], + "dest": "retention_days", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--direct-state-history-versions" + ], + "dest": "direct_state_history_versions", + "type": "int", + "group": 1 + }, + { + "kind": "append", + "options": [ + "--drop-old-name-view" + ], + "dest": "drop_old_name_views", + "default": [], + "metavar": "NAME", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--as" + ], + "dest": "as_target", + "help": "Preview the janitor plan as another configured target through the active target's connection; inspection only", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "init", + "parser": { + "prog": "sqb init", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "playground", + "parser": { + "prog": "sqb playground", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "playground_path", + "nargs": "?", + "default": "sqlbuild-playground", + "metavar": "path", + "group": 0 + }, + { + "kind": "store", + "options": [ + "--template" + ], + "dest": "playground_template", + "default": "waffle_shop", + "choices": [ + "waffle_shop", + "loader_waffle_shop", + "dagster", + "rivers", + "python_nodes" + ], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "scenario", + "parser": { + "prog": "sqb scenario", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "parsers", + "dest": "scenario_command", + "nargs": "A...", + "group": 0, + "parsers": [ + { + "name": "test", + "parser": { + "prog": "sqb scenario test", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "mutex_groups": [ + false + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--scenario-namespace" + ], + "dest": "scenario_namespace", + "metavar": "NAMESPACE", + "group": 1 + }, + { + "kind": "store", + "dest": "scenario_selector", + "nargs": "*", + "metavar": "scenario", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--retain" + ], + "dest": "scenario_retain", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--local" + ], + "dest": "scenario_local", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--strict" + ], + "dest": "scenario_strict", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--concurrency" + ], + "dest": "concurrency", + "type": "int", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--json-output" + ], + "dest": "json_output", + "type": "Path", + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--sync-snapshots" + ], + "dest": "scenario_sync_snapshots", + "nargs": 0, + "const": true, + "default": false, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--refresh" + ], + "dest": "scenario_refresh", + "nargs": 0, + "const": true, + "default": false, + "mutex_group": 0, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--force" + ], + "dest": "scenario_force", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-rows" + ], + "dest": "scenario_max_snapshot_rows", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-total-rows" + ], + "dest": "scenario_max_snapshot_total_rows", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-bytes" + ], + "dest": "scenario_max_snapshot_bytes", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-total-bytes" + ], + "dest": "scenario_max_snapshot_total_bytes", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "capture", + "parser": { + "prog": "sqb scenario capture", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--scenario-namespace" + ], + "dest": "scenario_namespace", + "metavar": "NAMESPACE", + "group": 1 + }, + { + "kind": "store", + "dest": "scenario_selector", + "nargs": "*", + "metavar": "scenario", + "group": 0 + }, + { + "kind": "store_true", + "options": [ + "--retain" + ], + "dest": "scenario_retain", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--force" + ], + "dest": "scenario_force", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-rows" + ], + "dest": "scenario_max_snapshot_rows", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-total-rows" + ], + "dest": "scenario_max_snapshot_total_rows", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-bytes" + ], + "dest": "scenario_max_snapshot_bytes", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--max-snapshot-total-bytes" + ], + "dest": "scenario_max_snapshot_total_bytes", + "type": "int", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--warehouse" + ], + "dest": "warehouse", + "help": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + } + ], + "choices_help": [] + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "dbt", + "parser": { + "prog": "sqb dbt", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "parsers", + "dest": "dbt_command", + "nargs": "A...", + "group": 0, + "parsers": [ + { + "name": "plan", + "parser": { + "prog": "sqb dbt plan", + "add_help": false, + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [] + } + }, + { + "name": "run", + "parser": { + "prog": "sqb dbt run", + "add_help": false, + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [] + } + }, + { + "name": "build", + "parser": { + "prog": "sqb dbt build", + "add_help": false, + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [] + } + }, + { + "name": "debug", + "parser": { + "prog": "sqb dbt debug", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + } + ] + } + }, + { + "name": "init", + "parser": { + "prog": "sqb dbt init", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir" + ], + "dest": "dbt_project_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--profiles-dir" + ], + "dest": "dbt_profiles_dir", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--profile" + ], + "dest": "dbt_profile", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--target" + ], + "dest": "dbt_target", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--sqb-output-dir" + ], + "dest": "sqb_output_dir", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--dry-run" + ], + "dest": "dry_run", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--overwrite" + ], + "dest": "overwrite", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--skip-dbt-debug" + ], + "dest": "skip_dbt_debug", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + } + ] + } + } + ], + "choices_help": [] + } + ] + } + }, + { + "name": "skills", + "parser": { + "prog": "sqb skills", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--global" + ], + "dest": "skills_global", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "append", + "options": [ + "--target" + ], + "dest": "skills_target", + "default": [], + "choices": [ + "opencode", + "claude", + "agents" + ], + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--force" + ], + "dest": "skills_force", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "rules", + "parser": { + "prog": "sqb rules", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--json" + ], + "dest": "json", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "parsers", + "dest": "rules_command", + "nargs": "A...", + "required": true, + "group": 0, + "parsers": [ + { + "name": "list", + "parser": { + "prog": "sqb rules list", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "show", + "parser": { + "prog": "sqb rules show", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "rules_rule_selector", + "required": true, + "group": 0 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "run", + "parser": { + "prog": "sqb rules run", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store", + "dest": "rules_rule_selector", + "required": true, + "group": 0 + }, + { + "kind": "extend", + "options": [ + "--select", + "-s" + ], + "dest": "select", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "append", + "options": [ + "--select-file" + ], + "dest": "select_file", + "default": [], + "group": 1 + }, + { + "kind": "extend", + "options": [ + "--exclude" + ], + "dest": "exclude", + "nargs": "+", + "default": [], + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + }, + { + "name": "skills", + "parser": { + "prog": "sqb rules skills", + "groups": [ + { + "title": "positional arguments" + }, + { + "title": "options" + } + ], + "actions": [ + { + "kind": "help", + "options": [ + "-h", + "--help" + ], + "dest": "help", + "nargs": 0, + "default": { + "suppress": true + }, + "help": "show this help message and exit", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--check" + ], + "dest": "rules_skills_check", + "nargs": 0, + "const": true, + "default": false, + "group": 1 + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + } + ], + "choices_help": [] + }, + { + "kind": "store", + "options": [ + "--project-dir", + "--sqb-project-dir" + ], + "dest": "project_dir", + "default": { + "suppress": true + }, + "help": "SQLBuild project directory (default: the current directory)", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--no-color" + ], + "dest": "no_color", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "disable colored output", + "group": 1 + }, + { + "kind": "store_true", + "options": [ + "--debug" + ], + "dest": "debug", + "nargs": 0, + "const": true, + "default": { + "suppress": true + }, + "help": "enable verbose output and mirror internal diagnostics to stderr", + "group": 1 + } + ] + } + } + ], + "choices_help": [ + { + "dest": "cost", + "metavar": "cost", + "help": "show persisted Snowflake busy-compute estimates" + }, + { + "dest": "rename", + "metavar": "rename", + "help": "rename a model or column and update every reference" + }, + { + "dest": "mv", + "metavar": "mv", + "help": "move a model file, optionally renaming it, and update references" + } + ] + } + ] +} \ No newline at end of file diff --git a/crates/sqlbuild-cli/src/argv/constants.rs b/crates/sqlbuild-cli/src/argv/constants.rs new file mode 100644 index 0000000000..9396f84758 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/constants.rs @@ -0,0 +1,57 @@ +//! The `sqb` command table, `argparse` vocabulary and the post-parse rules' names. + +/// The exported `sqb` parser tree: every parser, group and action in `add_argument` order. +pub(crate) const COMMAND_TABLE: &str = include_str!("command_table.json"); +/// `argparse.SUPPRESS` as a destination. +pub(crate) const SUPPRESS_DEST: &str = "==SUPPRESS=="; +/// The placeholder of the installed version in the `--version` text. +pub(crate) const VERSION_PLACEHOLDER: &str = "{version}"; +pub(crate) const PREFIX_CHARS: char = '-'; +pub(crate) const OPTIONS_TERMINATOR: &str = "--"; +pub(crate) const USAGE_PREFIX: &str = "usage: "; +/// `SqlbuildArgumentParser.error`'s coded prefix and its `CliStyle.error_strong` colours. +pub(crate) const ERROR_PREFIX: &str = "error[C900]:"; +pub(crate) const ERROR_STRONG_STYLE: &str = "\u{1b}[38;5;167m\u{1b}[1m"; +pub(crate) const STYLE_RESET: &str = "\u{1b}[0m"; +pub(crate) const PARSER_ERROR_EXIT: i64 = 2; +pub(crate) const HELP_EXIT: i64 = 0; +pub(crate) const INDENT_INCREMENT: i64 = 2; +pub(crate) const MAX_HELP_POSITION: i64 = 24; +pub(crate) const MINIMUM_TEXT_WIDTH: i64 = 11; +/// `_long_break_matcher` folds longer runs of newlines into this many. +pub(crate) const MAXIMUM_BLANK_RUN: usize = 2; +/// `textwrap`'s em-dash is at least this many hyphens. +pub(crate) const MINIMUM_EM_DASH: usize = 2; + +/// `CliNamespace` attributes the post-parse rules read and write. +pub(crate) const COMMAND_DEST: &str = "command"; +pub(crate) const DBT_COMMAND: &str = "dbt"; +pub(crate) const DBT_DEBUG_COMMAND: &str = "debug"; +pub(crate) const DBT_PASSTHROUGH_COMMANDS: [&str; 4] = ["plan", "run", "build", "debug"]; +pub(crate) const DBT_PASSTHROUGH_SEPARATOR: &str = "--"; +pub(crate) const DBT_VERBOSE_OPTIONS: [&str; 2] = ["--verbose", "-v"]; +pub(crate) const DBT_VERBOSE_OPTION: &str = "--verbose"; +pub(crate) const NO_COLOR_OPTION: &str = "--no-color"; +pub(crate) const DEBUG_OPTION: &str = "--debug"; +pub(crate) const POSITIONAL_SELECT_COMMANDS: [&str; 8] = [ + "audit", + "build", + "check", + "compile", + "freshness", + "plan", + "seed", + "test", +]; +pub(crate) const CONCURRENCY_COMMANDS: [&str; 5] = ["audit", "build", "load", "seed", "test"]; +pub(crate) const CURSOR_COMMANDS: [&str; 3] = ["build", "load", "plan"]; +pub(crate) const SCOPE_COMMAND: &str = "scope"; +pub(crate) const TEST_COMMAND: &str = "test"; +pub(crate) const JANITOR_COMMAND: &str = "janitor"; +pub(crate) const CONCURRENCY_ENV_VAR: &str = "SQLBUILD_CONCURRENCY"; +pub(crate) const SCOPE_GLOBAL_SUMMARY: &str = "summary"; +pub(crate) const SCOPE_DEFAULT_PAGE_SIZE: i64 = 100; +pub(crate) const SCOPE_KIND_DEST: &str = "scope_kind"; +pub(crate) const INT_CONVERTER: &str = "int"; +/// The `type=pathlib.Path` converter, which never rejects a string. +pub(crate) const PATH_CONVERTER: &str = "Path"; diff --git a/crates/sqlbuild-cli/src/argv/main/mod.rs b/crates/sqlbuild-cli/src/argv/main/mod.rs new file mode 100644 index 0000000000..cb9812b068 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/main/mod.rs @@ -0,0 +1 @@ +pub mod parse_cli_arguments; diff --git a/crates/sqlbuild-cli/src/argv/main/parse_cli_arguments.rs b/crates/sqlbuild-cli/src/argv/main/parse_cli_arguments.rs new file mode 100644 index 0000000000..2f75531a90 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/main/parse_cli_arguments.rs @@ -0,0 +1,33 @@ +//! Parse `sqb`'s argv as its `argparse` parser and `parse_cli_invocation` did. + +use crate::argv::_helpers::parsing::Run; +use crate::argv::_helpers::table::root_parser; +use crate::argv::models::{ArgvOutcome, ArgvSettings, Namespace, ParserSpec, Value}; +use crate::argv::types::ArgvHost; + +/// Parse `argv` onto a namespace holding `defaults`; the result, and everything to print. +pub fn parse_cli_arguments( + argv: &[String], + defaults: Vec<(String, Value)>, + settings: ArgvSettings, + host: &mut dyn ArgvHost, +) -> ArgvOutcome { + let parser: &ParserSpec = root_parser(); + let mut run: Run<'_> = Run { + settings: &settings, + host, + writes: Vec::new(), + }; + let namespace: Namespace = Namespace { + entries: defaults, + unrecognized: None, + }; + let parsed: Result = run + .parse_known_args(parser, argv.to_vec(), namespace) + .and_then(|known| run.normalized(parser, known)) + .map_err(|exit| exit.0); + ArgvOutcome { + writes: run.writes, + parsed, + } +} diff --git a/crates/sqlbuild-cli/src/argv/mod.rs b/crates/sqlbuild-cli/src/argv/mod.rs new file mode 100644 index 0000000000..918c269503 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/mod.rs @@ -0,0 +1,10 @@ +//! `sqb`'s argument parsing: an `argparse`-compatible parser over the embedded command table. + +pub(crate) mod _helpers; +pub(crate) mod constants; +pub mod main; +pub mod models; +pub mod types; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlbuild-cli/src/argv/models.rs b/crates/sqlbuild-cli/src/argv/models.rs new file mode 100644 index 0000000000..70042fdbea --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/models.rs @@ -0,0 +1,162 @@ +//! The `sqb` command table, parsed values and the outcome of one argv parse. + +use std::collections::HashMap; + +/// How many command-line strings an action consumes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Nargs { + /// `nargs=None`: exactly one string. + Single, + /// `nargs=0`: a flag. + Zero, + /// `nargs='?'`. + Optional, + /// `nargs='*'`. + ZeroOrMore, + /// `nargs='+'`. + OneOrMore, + /// `nargs='A...'`: a sub-command name and everything after it. + Parser, +} + +/// The `argparse` action class of one argument. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActionKind { + Store, + StoreTrue, + StoreFalse, + StoreConst, + Append, + Extend, + Help, + Version, + Parsers, +} + +/// A Python value the parse stores, as the binding rebuilds it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Value { + None, + Bool(bool), + /// A Python `int` as its canonical decimal text. + Int(String), + Str(String), + /// A `pathlib.Path` built from this text. + Path(String), + List(Vec), + /// An empty `dict`. + EmptyDict, + /// A value only the host can represent, by the handle it gave it. + Host(usize), +} + +/// An action default: a value, or `argparse.SUPPRESS`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Default { + Suppress, + Value(Value), +} + +/// Help text: none, `argparse.SUPPRESS`, or text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HelpText { + None, + Suppress, + Text(String), +} + +/// One sub-command's help line in its parent's help. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChoiceHelp { + pub metavar: String, + pub help: HelpText, +} + +/// One `add_argument` call of a parser. +#[derive(Debug, Clone)] +pub struct ActionSpec { + pub id: usize, + pub kind: ActionKind, + pub option_strings: Vec, + /// The destination name; `None` for `argparse.SUPPRESS`. + pub dest: Option, + pub nargs: Nargs, + pub constant: Value, + pub default: Default, + /// The `type=` converter by name: `int`, `Path`, `_positive_integer` or `parse_cli_vars`. + pub converter: Option, + pub choices: Option>, + pub required: bool, + pub help: HelpText, + /// The metavar: `None` when unset, `Some(None)` for `argparse.SUPPRESS`. + pub metavar: Option>, + pub group: usize, + pub mutex_group: Option, + pub version: Option, + /// Sub-command names and their parsers, in order. + pub parsers: Vec<(String, ParserSpec)>, + pub choices_help: Vec, +} + +/// One argument group's title and description. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GroupSpec { + pub title: Option, + pub description: Option, +} + +/// One `ArgumentParser`: its texts, groups and actions in order. +#[derive(Debug, Clone)] +pub struct ParserSpec { + pub prog: String, + pub usage: Option, + pub description: Option, + pub epilog: Option, + pub groups: Vec, + /// Whether each mutually exclusive group is required. + pub mutex_groups: Vec, + pub actions: Vec, + /// Each option string's action, in `add_argument` order. + pub option_actions: Vec<(String, usize)>, + pub option_index: HashMap, +} + +/// An `argparse.Namespace`: attributes in assignment order. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Namespace { + pub entries: Vec<(String, Value)>, + /// `_UNRECOGNIZED_ARGS_ATTR`, set while a sub-command's leftovers travel up. + pub unrecognized: Option>, +} + +/// Where one write of the parse goes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stream { + Stdout, + Stderr, +} + +/// What the caller decides about the parse's output. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArgvSettings { + /// The terminal width `shutil.get_terminal_size().columns` reports. + pub columns: i64, + /// Whether the `error[C900]:` prefix is coloured. + pub use_color: bool, +} + +/// Why a `type=` converter rejected a string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeFailure { + /// `argparse.ArgumentTypeError` with this message. + ArgumentType(String), + /// `TypeError` or `ValueError`: `invalid value: ''`. + Invalid, +} + +/// The writes of the parse, then either the namespace or the exit status. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArgvOutcome { + pub writes: Vec<(Stream, String)>, + pub parsed: Result, +} diff --git a/crates/sqlbuild-cli/src/argv/tests.rs b/crates/sqlbuild-cli/src/argv/tests.rs new file mode 100644 index 0000000000..2499094a55 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/tests.rs @@ -0,0 +1,8 @@ +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_parse_cli_arguments.rs"] +mod parse_cli_arguments; +#[path = "tests/test_types.rs"] +mod test_types; +#[path = "tests/test_text_wrap.rs"] +mod text_wrap; diff --git a/crates/sqlbuild-cli/src/argv/tests/helpers.rs b/crates/sqlbuild-cli/src/argv/tests/helpers.rs new file mode 100644 index 0000000000..ee18b96a67 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/tests/helpers.rs @@ -0,0 +1,23 @@ +use crate::argv::main::parse_cli_arguments::parse_cli_arguments; +use crate::argv::models::{ArgvOutcome, ArgvSettings, Value}; +use crate::argv::tests::test_types::FakeHost; + +/// Parse `argv` at the recorded 100-column width without colour. +pub(super) fn parse(argv: &[&str]) -> ArgvOutcome { + let argv: Vec = argv.iter().map(|arg| (*arg).to_owned()).collect(); + let settings: ArgvSettings = ArgvSettings { + columns: 100, + use_color: false, + }; + parse_cli_arguments(&argv, Vec::new(), settings, &mut FakeHost) +} + +/// A list of string values. +pub(super) fn strings(items: &[&str]) -> Value { + Value::List( + items + .iter() + .map(|item| Value::Str((*item).to_owned())) + .collect(), + ) +} diff --git a/crates/sqlbuild-cli/src/argv/tests/test_parse_cli_arguments.rs b/crates/sqlbuild-cli/src/argv/tests/test_parse_cli_arguments.rs new file mode 100644 index 0000000000..0e85966e2a --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/tests/test_parse_cli_arguments.rs @@ -0,0 +1,102 @@ +use crate::argv::models::{ArgvOutcome, Value}; +use crate::argv::tests::helpers::{parse, strings}; +use crate::argv::tests::test_types::{ExitTestCase, ParsedValueTestCase}; + +#[test] +fn given_accepted_arguments_when_parsing_then_attributes_hold_the_parsed_values() { + let test_cases = [ + ParsedValueTestCase { + description: "positional, trailing and --select selectors merge", + argv: &["build", "orders", "--select", "customers", "products"], + attribute: "select", + expected_value: strings(&["orders", "customers", "products"]), + }, + ParsedValueTestCase { + description: "an unset concurrency stays None", + argv: &["build"], + attribute: "concurrency", + expected_value: Value::None, + }, + ParsedValueTestCase { + description: "an abbreviated option with = keeps its value", + argv: &["build", "--conc=4"], + attribute: "concurrency", + expected_value: Value::Int("4".to_owned()), + }, + ParsedValueTestCase { + description: "dbt passthrough keeps arguments after the separator", + argv: &["dbt", "plan", "--no-color", "--", "--no-color"], + attribute: "dbt_args", + expected_value: strings(&["--", "--no-color"]), + }, + ParsedValueTestCase { + description: "a nested sub-command's dest is set", + argv: &["contract", "diff", "--from", "prod"], + attribute: "contract_command", + expected_value: Value::Str("diff".to_owned()), + }, + ]; + + for test_case in test_cases { + let outcome: ArgvOutcome = parse(test_case.argv); + let parsed = outcome.parsed.expect(test_case.description); + assert_eq!( + parsed.get(test_case.attribute), + Some(&test_case.expected_value), + "{}", + test_case.description + ); + } +} + +#[test] +fn given_help_version_or_rejected_arguments_when_parsing_then_exits_after_writing() { + let test_cases = [ + ExitTestCase { + description: "version prints the installed version", + argv: &["--version"], + expected_exit: 0, + expected_last_write: "sqb 1.2.3\n", + }, + ExitTestCase { + description: "an invalid integer is a coded parser error", + argv: &["build", "--concurrency", "x1"], + expected_exit: 2, + expected_last_write: "sqb build: error[C900]: argument --concurrency: invalid int value: 'x1'\n\n", + }, + ExitTestCase { + description: "an ambiguous abbreviation lists its matches", + argv: &["build", "--allow-s"], + expected_exit: 2, + expected_last_write: "sqb build: error[C900]: ambiguous option: --allow-s could match --allow-snapshot-full-refresh, --allow-snapshot-schema-change\n\n", + }, + ExitTestCase { + description: "a post-parse rule reports through the root parser", + argv: &["build", "--concurrency", "0"], + expected_exit: 2, + expected_last_write: "sqb: error[C900]: --concurrency must be >= 1\n\n", + }, + ExitTestCase { + description: "a missing required sub-command is named", + argv: &["rules"], + expected_exit: 2, + expected_last_write: "sqb rules: error[C900]: the following arguments are required: rules_command\n\n", + }, + ]; + + for test_case in test_cases { + let outcome: ArgvOutcome = parse(test_case.argv); + assert_eq!( + outcome.parsed, + Err(test_case.expected_exit), + "{}", + test_case.description + ); + assert_eq!( + outcome.writes.last().map(|(_, text)| text.as_str()), + Some(test_case.expected_last_write), + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-cli/src/argv/tests/test_text_wrap.rs b/crates/sqlbuild-cli/src/argv/tests/test_text_wrap.rs new file mode 100644 index 0000000000..4f936a0aef --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/tests/test_text_wrap.rs @@ -0,0 +1,42 @@ +use crate::argv::_helpers::text_wrap::{normalize_whitespace, wrap}; +use crate::argv::tests::test_types::WrapTestCase; + +#[test] +fn given_help_text_when_wrapping_then_lines_match_python_textwrap() { + let test_cases = [ + WrapTestCase { + description: "hyphenated words break after the hyphen; options stay whole", + text: " write a read-only report with --dry-run\n flags ", + width: 14, + indent: "", + expected_lines: &["write a read-", "only report", "with --dry-run", "flags"], + }, + WrapTestCase { + description: "words longer than the line are split", + text: "abcdefghij", + width: 4, + indent: " ", + expected_lines: &[" ab", " cd", " ef", " gh", " ij"], + }, + WrapTestCase { + description: "an em-dash between words is its own chunk", + text: "orders--customers", + width: 9, + indent: "", + expected_lines: &["orders--", "customers"], + }, + ]; + + for test_case in test_cases { + assert_eq!( + wrap( + &normalize_whitespace(test_case.text), + test_case.width, + test_case.indent + ), + test_case.expected_lines, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-cli/src/argv/tests/test_types.rs b/crates/sqlbuild-cli/src/argv/tests/test_types.rs new file mode 100644 index 0000000000..d500108f92 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/tests/test_types.rs @@ -0,0 +1,48 @@ +use sqlbuild_core::text::main::python_str_repr::python_str_repr; + +use crate::argv::models::{TypeFailure, Value}; +use crate::argv::types::ArgvHost; + +/// Integer conversion and a fixed version, standing in for the interpreter. +pub(super) struct FakeHost; + +impl ArgvHost for FakeHost { + fn convert(&mut self, _converter: &str, text: &str) -> Result { + text.parse::() + .map(|number| Value::Int(number.to_string())) + .map_err(|_| TypeFailure::Invalid) + } + + fn repr(&self, text: &str) -> String { + python_str_repr(text) + } + + fn program_version(&mut self) -> String { + "1.2.3".to_owned() + } +} + +/// An accepted command line and the value one attribute ends with. +pub(super) struct ParsedValueTestCase { + pub(super) description: &'static str, + pub(super) argv: &'static [&'static str], + pub(super) attribute: &'static str, + pub(super) expected_value: Value, +} + +/// A command line that ends the parse, its exit status and last write. +pub(super) struct ExitTestCase { + pub(super) description: &'static str, + pub(super) argv: &'static [&'static str], + pub(super) expected_exit: i64, + pub(super) expected_last_write: &'static str, +} + +/// Text, width and indent for `textwrap.wrap`, and the expected lines. +pub(super) struct WrapTestCase { + pub(super) description: &'static str, + pub(super) text: &'static str, + pub(super) width: usize, + pub(super) indent: &'static str, + pub(super) expected_lines: &'static [&'static str], +} diff --git a/crates/sqlbuild-cli/src/argv/types.rs b/crates/sqlbuild-cli/src/argv/types.rs new file mode 100644 index 0000000000..baa6f46907 --- /dev/null +++ b/crates/sqlbuild-cli/src/argv/types.rs @@ -0,0 +1,13 @@ +//! What the parse asks of the interpreter it runs for. + +use crate::argv::models::{TypeFailure, Value}; + +/// The interpreter-owned pieces of a parse: converters, `repr` and version. +pub trait ArgvHost { + /// Apply the `type=` converter named `converter` to `text`. + fn convert(&mut self, converter: &str, text: &str) -> Result; + /// Python's `repr` of a command-line string. + fn repr(&self, text: &str) -> String; + /// The installed `sqlbuild` version `--version` prints. + fn program_version(&mut self) -> String; +} diff --git a/crates/sqlbuild-cli/src/lib.rs b/crates/sqlbuild-cli/src/lib.rs index c06ee4903e..b275aebc62 100644 --- a/crates/sqlbuild-cli/src/lib.rs +++ b/crates/sqlbuild-cli/src/lib.rs @@ -2,6 +2,7 @@ #![forbid(unsafe_code)] +pub mod argv; pub mod compute_logs; pub mod launcher; pub mod replay; diff --git a/crates/sqlbuild-python/Cargo.toml b/crates/sqlbuild-python/Cargo.toml index 292ccdc015..a434dcb14c 100644 --- a/crates/sqlbuild-python/Cargo.toml +++ b/crates/sqlbuild-python/Cargo.toml @@ -24,6 +24,7 @@ serde_json.workspace = true sqlbuild-analysis.workspace = true sqlbuild-attachments.workspace = true sqlbuild-cache.workspace = true +sqlbuild-cli.workspace = true sqlbuild-config.workspace = true sqlbuild-core.workspace = true sqlbuild-discovery.workspace = true diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs new file mode 100644 index 0000000000..901810863c --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/argv.rs @@ -0,0 +1,242 @@ +//! `sqb`'s argument parsing: the native parser with Python converters and output. + +use pyo3::exceptions::{PyAttributeError, PyOSError, PyValueError}; +use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::types::{ + PyBool, PyBoolMethods, PyDict, PyDictMethods, PyInt, PyList, PyListMethods, PyString, +}; +use pyo3::{pyfunction, wrap_pyfunction}; +use sqlbuild_cli::argv::main::parse_cli_arguments::parse_cli_arguments as parse_arguments; +use sqlbuild_cli::argv::models::{ArgvOutcome, ArgvSettings, Stream, TypeFailure, Value}; +use sqlbuild_cli::argv::types::ArgvHost; +use sqlbuild_core::text::main::python_str_repr::python_str_repr; + +use crate::bindings::_helpers::cli::surrogates::{python_text, rust_text, uses_placeholders}; + +const INT_CONVERTER: &str = "int"; +const POSITIVE_INTEGER_CONVERTER: &str = "_positive_integer"; +const VARS_CONVERTER: &str = "parse_cli_vars"; +const DISTRIBUTION: &str = "sqlbuild"; +const UNKNOWN_VERSION: &str = "unknown"; + +/// Parsed attributes as `(name, value)` pairs, or `None`; then the exit status, or `None`. +type ParsedRows = (Option)>>, Option); + +/// The interpreter side of a parse: converters, `repr` and the installed version. +struct PythonHost<'py> { + py: Python<'py>, + objects: Vec>, + /// Whether command-line text carries lone surrogates as placeholders. + surrogates: bool, +} + +impl<'py> PythonHost<'py> { + /// Rust text as the Python `str` it came from. + fn text(&self, text: &str) -> PyResult> { + python_text(self.py, text, self.surrogates) + } + + fn keep(&mut self, object: Bound<'_, PyAny>) -> Value { + self.objects.push(object.unbind()); + Value::Host(self.objects.len() - 1) + } + + fn integer(&self, text: &str) -> Result, TypeFailure> { + self.text(text) + .and_then(|text| self.py.get_type::().call1((text,))) + .map_err(|_| TypeFailure::Invalid) + } + + fn vars(&mut self, text: &str) -> PyResult> { + let json: Bound<'_, PyModule> = self.py.import("json")?; + let parsed: Bound<'_, PyAny> = match json.getattr("loads")?.call1((self.text(text)?,)) { + Ok(parsed) => parsed, + Err(error) => { + let decode_error: Bound<'_, PyAny> = json.getattr("JSONDecodeError")?; + if !error.value(self.py).is_instance(&decode_error)? { + return Err(error); + } + let message: String = error.value(self.py).getattr("msg")?.extract()?; + return Ok(Err(TypeFailure::ArgumentType(format!( + "--vars must be valid JSON: {message}" + )))); + } + }; + let Ok(parsed) = parsed.downcast_into::() else { + return Ok(Err(TypeFailure::ArgumentType( + "--vars must be a JSON object".to_owned(), + ))); + }; + Ok(Ok(self.keep(parsed.into_any()))) + } +} + +impl ArgvHost for PythonHost<'_> { + fn convert(&mut self, converter: &str, text: &str) -> Result { + match converter { + INT_CONVERTER => { + let number: Bound<'_, PyAny> = self.integer(text)?; + Ok(Value::Int( + number.str().map_err(|_| TypeFailure::Invalid)?.to_string(), + )) + } + POSITIVE_INTEGER_CONVERTER => { + let number: Bound<'_, PyAny> = self.integer(text)?; + if number.lt(1).unwrap_or(true) { + return Err(TypeFailure::ArgumentType( + "value must be a positive integer".to_owned(), + )); + } + Ok(Value::Int( + number.str().map_err(|_| TypeFailure::Invalid)?.to_string(), + )) + } + VARS_CONVERTER => self.vars(text).unwrap_or(Err(TypeFailure::Invalid)), + _ => Err(TypeFailure::Invalid), + } + } + + fn repr(&self, text: &str) -> String { + self.text(text) + .and_then(|text| text.repr()) + .map(|repr| repr.to_string()) + .unwrap_or_else(|_| python_str_repr(text)) + } + + fn program_version(&mut self) -> String { + self.py + .import("importlib.metadata") + .and_then(|metadata| metadata.getattr("version")?.call1((DISTRIBUTION,))) + .and_then(|version| version.extract()) + .unwrap_or_else(|_| UNKNOWN_VERSION.to_owned()) + } +} + +impl PythonHost<'_> { + /// A Python default as a parse value; anything the parse does not read stays a host object. + fn value_of(&mut self, object: &Bound<'_, PyAny>) -> PyResult { + if object.is_none() { + return Ok(Value::None); + } + if let Ok(flag) = object.downcast::() { + return Ok(Value::Bool(flag.is_true())); + } + if object.is_exact_instance_of::() { + return Ok(Value::Int(object.str()?.to_string())); + } + if object.is_exact_instance_of::() { + return Ok(Value::Str(object.extract()?)); + } + if let Ok(items) = object.downcast_exact::() { + let mut values: Vec = Vec::with_capacity(items.len()); + for item in items.iter() { + values.push(self.value_of(&item)?); + } + return Ok(Value::List(values)); + } + if let Ok(dict) = object.downcast_exact::() + && dict.is_empty() + { + return Ok(Value::EmptyDict); + } + Ok(self.keep(object.clone())) + } +} + +/// A parse value as the Python object `argparse` would have stored. +fn object_of(py: Python<'_>, host: &PythonHost<'_>, value: Value) -> PyResult> { + Ok(match value { + Value::None => py.None(), + Value::Bool(flag) => PyBool::new(py, flag).to_owned().into_any().unbind(), + Value::Int(text) => py.get_type::().call1((text,))?.unbind(), + Value::Str(text) => host.text(&text)?.unbind(), + Value::Path(text) => py + .import("pathlib")? + .getattr("Path")? + .call1((host.text(&text)?,))? + .unbind(), + Value::List(items) => { + let list: Bound<'_, PyList> = PyList::empty(py); + for item in items { + list.append(object_of(py, host, item)?)?; + } + list.into_any().unbind() + } + Value::EmptyDict => PyDict::new(py).into_any().unbind(), + Value::Host(index) => host + .objects + .get(index) + .map(|object| object.clone_ref(py)) + .ok_or_else(|| PyValueError::new_err("unknown parsed value handle"))?, + }) +} + +/// Parse `argv` onto `defaults` as `sqb` did, printing its output; the attributes or exit status. +#[pyfunction] +#[pyo3(signature = (argv, defaults, *, use_color, columns))] +fn parse_cli_arguments( + argv: Vec>, + defaults: &Bound<'_, PyList>, + use_color: bool, + columns: i64, +) -> PyResult { + let py: Python<'_> = defaults.py(); + let mut arguments: Vec = Vec::with_capacity(argv.len()); + let mut plain_placeholders: bool = false; + let mut surrogates: bool = false; + for arg in &argv { + let (text, mapped) = rust_text(arg)?; + surrogates |= mapped; + plain_placeholders |= !mapped && uses_placeholders(&text); + arguments.push(text); + } + if surrogates && plain_placeholders { + return Err(PyValueError::new_err( + "a command-line argument mixes undecodable bytes with U+10F800..U+10FFFF", + )); + } + let mut host: PythonHost<'_> = PythonHost { + py, + objects: Vec::new(), + surrogates, + }; + let mut values: Vec<(String, Value)> = Vec::with_capacity(defaults.len()); + for item in defaults.iter() { + let (name, object): (String, Bound<'_, PyAny>) = item.extract()?; + values.push((name, host.value_of(&object)?)); + } + let settings: ArgvSettings = ArgvSettings { columns, use_color }; + let outcome: ArgvOutcome = parse_arguments(&arguments, values, settings, &mut host); + let sys: Bound<'_, PyModule> = py.import("sys")?; + for (stream, text) in outcome.writes { + let name: &str = match stream { + Stream::Stdout => "stdout", + Stream::Stderr => "stderr", + }; + let file: Bound<'_, PyAny> = sys.getattr(name)?; + if file.is_none() { + continue; + } + if let Err(error) = file.call_method1("write", (host.text(&text)?,)) + && !(error.is_instance_of::(py) + || error.is_instance_of::(py)) + { + return Err(error); + } + } + match outcome.parsed { + Err(code) => Ok((None, Some(code))), + Ok(namespace) => { + let mut rows: Vec<(String, Py)> = Vec::with_capacity(namespace.entries.len()); + for (name, value) in namespace.entries { + rows.push((name, object_of(py, &host, value)?)); + } + Ok((Some(rows), None)) + } + } +} + +pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(wrap_pyfunction!(parse_cli_arguments, module)?)?; + Ok(()) +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/mod.rs new file mode 100644 index 0000000000..c649fdb915 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod argv; +pub(crate) mod surrogates; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/cli/surrogates.rs b/crates/sqlbuild-python/src/bindings/_helpers/cli/surrogates.rs new file mode 100644 index 0000000000..572ee26520 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/cli/surrogates.rs @@ -0,0 +1,98 @@ +//! Command-line strings with lone surrogates (undecodable bytes) carried through Rust text. + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python}; +use pyo3::types::{PyBytes, PyBytesMethods, PyString, PyStringMethods}; + +/// Lone surrogates `U+D800..=U+DFFF` stand in Rust text as this private-use block, in order. +const PLACEHOLDER_START: u32 = 0x10_F800; +const SURROGATE_START: u32 = 0xD800; +const SURROGATE_COUNT: u32 = 0x800; +const SURROGATE_LEAD_BYTE: u8 = 0xED; +const SURROGATE_SECOND_MASK: u8 = 0xE0; +const SURROGATE_SECOND_BITS: u8 = 0xA0; +const PLACEHOLDER_LEAD_BYTES: [u8; 2] = [0xF4, 0x8F]; +const CONTINUATION_MASK: u8 = 0x3F; +const CONTINUATION_BITS: u8 = 0x80; +const SURROGATE_PREFIX_BITS: u32 = 0xD000; +const ENCODING: &str = "utf-8"; +const SURROGATE_HANDLER: &str = "surrogatepass"; + +/// A command-line string as Rust text, and whether it held lone surrogates. +pub(crate) fn rust_text(text: &Bound<'_, PyString>) -> PyResult<(String, bool)> { + if let Ok(plain) = text.to_str() { + return Ok((plain.to_owned(), false)); + } + let encoded: Bound<'_, PyAny> = text.call_method1("encode", (ENCODING, SURROGATE_HANDLER))?; + let bytes: &[u8] = encoded.downcast::()?.as_bytes(); + let mut mapped: Vec = Vec::with_capacity(bytes.len() + 1); + let mut index: usize = 0; + while index < bytes.len() { + let window: &[u8] = &bytes[index..bytes.len().min(index + 3)]; + if let [SURROGATE_LEAD_BYTE, second, third] = window + && second & SURROGATE_SECOND_MASK == SURROGATE_SECOND_BITS + { + let code: u32 = SURROGATE_PREFIX_BITS + | (u32::from(second & CONTINUATION_MASK) << 6) + | u32::from(third & CONTINUATION_MASK); + let placeholder: char = char::from_u32(PLACEHOLDER_START + code - SURROGATE_START) + .ok_or_else(|| PyValueError::new_err("invalid surrogate"))?; + mapped.extend_from_slice(placeholder.encode_utf8(&mut [0; 4]).as_bytes()); + index += 3; + continue; + } + mapped.push(bytes[index]); + index += 1; + } + let mapped: String = + String::from_utf8(mapped).map_err(|error| PyValueError::new_err(error.to_string()))?; + if holds_placeholders(bytes) { + return Err(PyValueError::new_err( + "a command-line argument mixes undecodable bytes with U+10F800..U+10FFFF", + )); + } + Ok((mapped, true)) +} + +fn holds_placeholders(bytes: &[u8]) -> bool { + bytes.windows(3).any(|window| { + window[..2] == PLACEHOLDER_LEAD_BYTES + && window[2] & SURROGATE_SECOND_MASK == SURROGATE_SECOND_BITS + }) +} + +/// Whether Rust text holds a character the surrogate placeholders use. +pub(crate) fn uses_placeholders(text: &str) -> bool { + text.chars() + .any(|character| placeholder_code(character).is_some()) +} + +fn placeholder_code(character: char) -> Option { + let code: u32 = u32::from(character); + (PLACEHOLDER_START..PLACEHOLDER_START + SURROGATE_COUNT) + .contains(&code) + .then(|| SURROGATE_START + code - PLACEHOLDER_START) +} + +/// Rust text back as a Python `str`, restoring lone surrogates when `surrogates` is set. +pub(crate) fn python_text<'py>( + py: Python<'py>, + text: &str, + surrogates: bool, +) -> PyResult> { + if !surrogates || !uses_placeholders(text) { + return Ok(PyString::new(py, text).into_any()); + } + let mut bytes: Vec = Vec::with_capacity(text.len()); + for character in text.chars() { + match placeholder_code(character) { + Some(code) => bytes.extend_from_slice(&[ + SURROGATE_LEAD_BYTE, + CONTINUATION_BITS | ((code >> 6) as u8 & CONTINUATION_MASK), + CONTINUATION_BITS | (code as u8 & CONTINUATION_MASK), + ]), + None => bytes.extend_from_slice(character.encode_utf8(&mut [0; 4]).as_bytes()), + } + } + PyBytes::new(py, &bytes).call_method1("decode", (ENCODING, SURROGATE_HANDLER)) +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/mod.rs index cb760a85cc..64b6b9f9d4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod analysis_session; pub(crate) mod attachments; pub(crate) mod boundary; pub(crate) mod cache; +pub(crate) mod cli; pub(crate) mod compile_attachment; pub(crate) mod compile_driver; pub(crate) mod compile_outputs; diff --git a/crates/sqlbuild-python/src/bindings/main/register.rs b/crates/sqlbuild-python/src/bindings/main/register.rs index b1e9de0921..0a1f0971de 100644 --- a/crates/sqlbuild-python/src/bindings/main/register.rs +++ b/crates/sqlbuild-python/src/bindings/main/register.rs @@ -4,10 +4,10 @@ use sqlbuild_rules::constants::{API_VERSION, NATIVE_BUILD_IDENTITY}; use crate::bindings::_helpers::boundary::panics::NativeCompilerError; use crate::bindings::_helpers::{ - analysis, analysis_session, attachments, boundary, cache, compile_attachment, compile_driver, - compile_outputs, compiled_project, contracts, dag_artifact, discovery, fingerprints, graph, - lineage, manifest, model_config, project_assembly, refactor, render, rules, scopes, - semantic_checks, sql_test_glue, sqltext, type_system, + analysis, analysis_session, attachments, boundary, cache, cli, compile_attachment, + compile_driver, compile_outputs, compiled_project, contracts, dag_artifact, discovery, + fingerprints, graph, lineage, manifest, model_config, project_assembly, refactor, render, + rules, scopes, semantic_checks, sql_test_glue, sqltext, type_system, }; use crate::bindings::models; @@ -16,6 +16,7 @@ pub fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { rules::evaluation::register(module)?; rules::lint::register(module)?; rules::skills::register(module)?; + cli::argv::register(module)?; module.add( "NativeCompilerError", module.py().get_type::(), diff --git a/python-boundary.toml b/python-boundary.toml index b7e53da950..ac0f277ea9 100644 --- a/python-boundary.toml +++ b/python-boundary.toml @@ -2,7 +2,7 @@ # migration work that moves to Rust. `remaining_lines` must equal the current count of that work: # a PR that moves code lowers it, and adding movable Python fails the check. # Native failures must surface as errors; `handled_native_errors` lists the reviewed exceptions. -remaining_lines = 125499 +remaining_lines = 124254 [[stays]] path = "executor" diff --git a/src/sqlbuild/_native.pyi b/src/sqlbuild/_native.pyi index 899d0a65d9..1dfd46f5d4 100644 --- a/src/sqlbuild/_native.pyi +++ b/src/sqlbuild/_native.pyi @@ -1599,6 +1599,9 @@ class SqlReferenceScanner: def scope_expected_model_names( texts: list[tuple[str, str]], scenario: bool, syntax: dict[str, object] ) -> list[tuple[str | None, list[str]]]: ... +def parse_cli_arguments( + argv: list[str], defaults: list[tuple[str, object]], *, use_color: bool, columns: int +) -> tuple[list[tuple[str, object]] | None, int | None]: ... def scope_test_ctes( texts: list[tuple[str, str]], syntax: dict[str, object] ) -> list[tuple[str | None, list[tuple[str, str]]]]: ... diff --git a/src/sqlbuild/cli/commands/_helpers/entry/errors.py b/src/sqlbuild/cli/commands/_helpers/entry/errors.py index e4e12a8537..8396667adf 100644 --- a/src/sqlbuild/cli/commands/_helpers/entry/errors.py +++ b/src/sqlbuild/cli/commands/_helpers/entry/errors.py @@ -5,20 +5,10 @@ import sys from collections.abc import Callable, Sequence -from sqlbuild.cli.commands.classes.sqlbuild_argument_parser import SqlbuildArgumentParser from sqlbuild.cli.commands.constants import NO_COLOR_OPTION from sqlbuild.presentation.main.coded_error_text import format_coded_error -def build_argument_parser_class(*, use_color: bool) -> type[SqlbuildArgumentParser]: - class ColorAwareSqlbuildArgumentParser(SqlbuildArgumentParser): - pass - - ColorAwareSqlbuildArgumentParser.use_color = use_color - - return ColorAwareSqlbuildArgumentParser - - def expected_error_parts(*, error: Exception, fallback_code: str) -> tuple[str, str, str | None]: code: str = str(getattr(error, "code", fallback_code)) message: str = str(getattr(error, "message", str(error))) diff --git a/src/sqlbuild/cli/commands/_helpers/entry/parser.py b/src/sqlbuild/cli/commands/_helpers/entry/parser.py deleted file mode 100644 index ba08ac6710..0000000000 --- a/src/sqlbuild/cli/commands/_helpers/entry/parser.py +++ /dev/null @@ -1,870 +0,0 @@ -"""Root CLI parser construction.""" - -from __future__ import annotations - -import argparse -from importlib.metadata import PackageNotFoundError, version -from pathlib import Path -from typing import cast - -from sqlbuild.cli.commands._helpers.entry.errors import build_argument_parser_class -from sqlbuild.cli.commands._helpers.entry.parser_arguments import ( - add_cursor_override_args, - add_dbt_config_args, - add_execution_args, - add_execution_json_output_arg, - add_microbatch_limit_override_arg, - add_positional_select_arg, - add_scenario_snapshot_safety_args, - add_select_args, - add_vars_args, -) -from sqlbuild.cli.commands.classes.sqlbuild_argument_parser import SqlbuildArgumentParser -from sqlbuild.cli.commands.constants import ( - COLUMN_LINEAGE_MODE_VALUES, - COMMAND_WAREHOUSE_GROUPS, - COMPILE_LINEAGE_MODE_VALUES, - JSON_OUTPUT_FORMAT, - PLAYGROUND_TEMPLATE_VALUES, -) -from sqlbuild.cli.commands.types import CliCommand, CompileLineageMode -from sqlbuild.compiler.contract_adoption.types import ContractAction -from sqlbuild.compiler.lineage.types import ColumnLineageMode -from sqlbuild.compiler.scopes.types import DeclarationKind - - -def _installed_version() -> str: - """Return the installed sqlbuild distribution version.""" - - try: - return version("sqlbuild") - except PackageNotFoundError: - return "unknown" - - -def build_cli_parser(*, use_color: bool = False) -> argparse.ArgumentParser: - """Build the root CLI parser.""" - - parser_class: type[SqlbuildArgumentParser] = build_argument_parser_class(use_color=use_color) - parser: argparse.ArgumentParser = parser_class(prog="sqb") - parser.add_argument( - "--version", - "-V", - action="version", - version=f"sqb {_installed_version()}", - ) - _add_global_args(parser=parser, suppress_defaults=False) - - subparsers: argparse._SubParsersAction[argparse.ArgumentParser] = parser.add_subparsers( - dest="command", - parser_class=parser_class, - ) - _add_compile_and_dag_parsers(subparsers) - _add_plan_and_build_parsers(subparsers) - _add_quality_parsers(subparsers) - _add_data_parsers(subparsers) - _add_inspection_parsers(subparsers) - _add_contract_parser(subparsers) - _add_refactor_parsers(subparsers) - _add_maintenance_parsers(subparsers) - _add_workspace_parsers(subparsers) - _add_dbt_parsers(subparsers) - _add_skills_parsers(subparsers) - _add_rules_parser(subparsers) - _add_warehouse_args(subparsers) - _add_global_args_to_subcommands(subparsers) - return parser - - -def _add_global_args(*, parser: argparse.ArgumentParser, suppress_defaults: bool) -> None: - """Add flags accepted before or after the subcommand; subcommand copies suppress defaults.""" - - parser.add_argument( - "--project-dir", - "--sqb-project-dir", - dest="project_dir", - default=argparse.SUPPRESS if suppress_defaults else None, - help="SQLBuild project directory (default: the current directory)", - ) - parser.add_argument( - "--no-color", - action="store_true", - default=argparse.SUPPRESS if suppress_defaults else False, - help="disable colored output", - ) - parser.add_argument( - "--debug", - action="store_true", - default=argparse.SUPPRESS if suppress_defaults else False, - help="enable verbose output and mirror internal diagnostics to stderr", - ) - - -def _add_global_args_to_subcommands( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - command_name: str - command_parser: argparse.ArgumentParser - for command_name, command_parser in subparsers.choices.items(): - if command_name == CliCommand.DBT: - continue - _add_global_args(parser=command_parser, suppress_defaults=True) - nested: argparse._SubParsersAction[argparse.ArgumentParser] - for nested in _nested_subparsers(command_parser): - _add_global_args_to_subcommands(nested) - - -def _nested_subparsers( - parser: argparse.ArgumentParser, -) -> list[argparse._SubParsersAction[argparse.ArgumentParser]]: - return [ - cast("argparse._SubParsersAction[argparse.ArgumentParser]", action) - for action in parser._actions - if isinstance(action, argparse._SubParsersAction) - ] - - -def _add_warehouse_args( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - command_name: str - command_parser: argparse.ArgumentParser - for command_name, command_parser in subparsers.choices.items(): - if command_name == CliCommand.DBT: - continue - if COMMAND_WAREHOUSE_GROUPS.get(CliCommand(command_name)) is None: - continue - _add_warehouse_arg_to_leaves(command_parser) - - -def _add_warehouse_arg_to_leaves(parser: argparse.ArgumentParser) -> None: - nested: list[argparse._SubParsersAction[argparse.ArgumentParser]] = _nested_subparsers(parser) - if not nested: - parser.add_argument( - "--warehouse", - default=None, - help=( - "warehouse for this invocation; overrides the target's command-group " - "warehouse and the connection warehouse" - ), - ) - return - action: argparse._SubParsersAction[argparse.ArgumentParser] - for action in nested: - child: argparse.ArgumentParser - for child in action.choices.values(): - _add_warehouse_arg_to_leaves(child) - - -def _add_sql_analysis_override(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "--no-sql-analysis", - "--no-sql-validation", - dest="no_sql_validation", - action="store_true", - default=False, - help="disable SQL syntax, binding, type inference, and semantic validation", - ) - - -def _add_contract_parser( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - contract_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.CONTRACT) - actions: argparse._SubParsersAction[argparse.ArgumentParser] = contract_parser.add_subparsers( - dest="contract_command", - required=True, - ) - for action in ContractAction: - action_parser: argparse.ArgumentParser = actions.add_parser(action) - action_parser.add_argument("--from", dest="contract_from", required=True) - action_parser.add_argument("--json", action="store_true", default=False) - _ = add_select_args(action_parser) - _ = add_vars_args(action_parser) - if action == ContractAction.GENERATE: - action_parser.add_argument("--write", dest="contract_write", action="store_true") - action_parser.add_argument("--overwrite", action="store_true") - - -def _add_refactor_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - rename_parser: argparse.ArgumentParser = subparsers.add_parser( - CliCommand.RENAME, help="rename a model or column and update every reference" - ) - rename_parser.add_argument( - "refactor_target", - metavar="TARGET", - help=" or .; the model: and column: prefixes are optional", - ) - rename_parser.add_argument("refactor_new_name", metavar="NEW_NAME") - rename_parser.add_argument( - "--cascade", - dest="refactor_cascade", - action="store_true", - default=False, - help="rename a column through every downstream model that passes it through", - ) - move_parser: argparse.ArgumentParser = subparsers.add_parser( - CliCommand.MV, help="move a model file, optionally renaming it, and update references" - ) - move_parser.add_argument( - "refactor_target", metavar="TARGET", help="; the model: prefix is optional" - ) - move_parser.add_argument( - "refactor_destination", metavar="DESTINATION", help="new .sql path or folder/" - ) - parser: argparse.ArgumentParser - for parser in (rename_parser, move_parser): - parser.add_argument( - "--dry-run", - dest="dry_run", - action="store_true", - default=False, - help="print every edit and verify it compiles, without writing files", - ) - parser.add_argument( - "--json", - action="store_true", - default=False, - help="print the result as JSON on stdout; progress goes to stderr", - ) - - -def _add_compile_and_dag_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - compile_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.COMPILE) - _add_sql_analysis_override(compile_parser) - compile_parser.add_argument( - "--no-cache", - action="store_true", - default=False, - help="Bypass the compile analysis cache for this invocation", - ) - compile_parser.add_argument("--defer-to", default=None) - compile_parser.add_argument("--target", default=None) - compile_parser.add_argument("--json", action="store_true", default=False) - compile_parser.add_argument("--manifest", action="store_true", default=False) - compile_parser.add_argument("--dag", nargs="?", const="", default=None) - compile_parser.add_argument( - "--lineage-mode", - dest="compile_lineage_mode", - choices=COMPILE_LINEAGE_MODE_VALUES, - default=CompileLineageMode.FAST.value, - help="Column lineage mode: fast (default), rich (slower), or none", - ) - compile_parser.add_argument( - "--profile-skip-discovery-sql-analysis", - action="store_true", - default=False, - help="Diagnostic: skip SQL analysis-assisted discovery parsing", - ) - compile_parser.add_argument( - "--profile-skip-column-inference", - action="store_true", - default=False, - help="Diagnostic: skip compile-time column inference", - ) - compile_parser.add_argument( - "--profile-skip-contracts", - action="store_true", - default=False, - help="Diagnostic: skip offline contract validation", - ) - compile_parser.add_argument( - "--profile-skip-write", - action="store_true", - default=False, - help="Diagnostic: skip writing target/compiled artifacts", - ) - add_select_args(compile_parser) - _ = add_positional_select_arg(compile_parser) - _ = add_vars_args(compile_parser) - _ = add_dbt_config_args(parser=compile_parser) - - dag_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.DAG) - _add_sql_analysis_override(dag_parser) - dag_parser.add_argument("--json", action="store_true", default=False) - _ = add_vars_args(dag_parser) - _ = add_dbt_config_args(parser=dag_parser) - - -def _add_plan_and_build_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - plan_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.PLAN) - _add_sql_analysis_override(plan_parser) - plan_parser.add_argument( - "--no-cache", - action="store_true", - default=False, - help="Bypass the compile analysis cache for this invocation", - ) - plan_parser.add_argument("--defer-to", default=None) - plan_parser.add_argument("--defer-sources-to", default=None) - plan_parser.add_argument("--target", default=None) - plan_parser.add_argument( - "--as", - dest="as_target", - default=None, - help=( - "Preview the plan as another configured target through the active target's " - "connection; inspection only" - ), - ) - plan_parser.add_argument("--json", action="store_true", default=False) - plan_parser.add_argument("--full-refresh", action="store_true", default=False) - plan_parser.add_argument( - "--selection-diagnostics", - action="store_true", - default=False, - help="Include direct-mode stale-out-of-selection diagnostics.", - ) - plan_parser.add_argument( - "--no-python", dest="include_python", action="store_false", default=True - ) - plan_parser.add_argument("--verbose", "-v", action="store_true", default=False) - plan_load_group: argparse._MutuallyExclusiveGroup = plan_parser.add_mutually_exclusive_group() - plan_load_group.add_argument("--load", dest="load_sources", action="store_true", default=None) - plan_load_group.add_argument("--no-load", dest="load_sources", action="store_false") - _ = add_cursor_override_args(plan_parser) - _ = add_microbatch_limit_override_arg(plan_parser) - _ = add_select_args(plan_parser) - _ = add_positional_select_arg(plan_parser) - _ = add_vars_args(plan_parser) - _ = add_dbt_config_args(parser=plan_parser) - - build_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.BUILD) - _add_sql_analysis_override(build_parser) - build_parser.add_argument( - "--no-cache", - action="store_true", - default=False, - help="Bypass the compile analysis cache for this invocation", - ) - build_parser.add_argument("--defer-to", default=None) - build_parser.add_argument("--defer-clone-from", default=None) - build_parser.add_argument("--defer-sources-to", default=None) - build_parser.add_argument("--target", default=None) - build_parser.add_argument("--json", action="store_true", default=False) - build_parser.add_argument( - "--selection-diagnostics", - action="store_true", - default=False, - help="Include direct-mode stale-out-of-selection diagnostics.", - ) - build_parser.add_argument( - "--no-python", dest="include_python", action="store_false", default=True - ) - build_parser.add_argument("--no-tests", dest="run_tests", action="store_false", default=True) - build_parser.add_argument("--no-audits", dest="run_audits", action="store_false", default=True) - build_parser.add_argument("--manifest", action="store_true", default=False) - _ = add_execution_json_output_arg(build_parser) - build_parser.add_argument("--event-output", type=Path, default=None, help=argparse.SUPPRESS) - _ = add_cursor_override_args(build_parser) - _ = add_microbatch_limit_override_arg(build_parser) - build_load_group: argparse._MutuallyExclusiveGroup = build_parser.add_mutually_exclusive_group() - build_load_group.add_argument("--load", dest="load_sources", action="store_true", default=None) - build_load_group.add_argument("--no-load", dest="load_sources", action="store_false") - build_load_group.add_argument("--reload", dest="reload", action="store_true", default=False) - _ = add_execution_args(build_parser) - _ = add_select_args(build_parser) - _ = add_positional_select_arg(build_parser) - _ = add_vars_args(build_parser) - _ = add_dbt_config_args(parser=build_parser) - - -def _add_quality_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - freshness_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.FRESHNESS) - _add_sql_analysis_override(freshness_parser) - freshness_parser.add_argument("--json", action="store_true", default=False) - freshness_parser.add_argument("--state", action="store_true", default=False) - freshness_parser.add_argument("--target", default=None) - freshness_parser.add_argument("--fail-on-error", action="store_true", default=False) - freshness_parser.add_argument("--fail-on-stale", action="store_true", default=False) - _ = add_execution_json_output_arg(freshness_parser) - _ = add_select_args(freshness_parser) - _ = add_positional_select_arg(freshness_parser) - _ = add_vars_args(freshness_parser) - _ = add_dbt_config_args(parser=freshness_parser) - - test_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.TEST) - _add_sql_analysis_override(test_parser) - test_parser.add_argument("--json", action="store_true", default=False) - test_parser.add_argument("--target", default=None) - test_parser.add_argument("--concurrency", type=int, default=None) - test_parser.add_argument("--case", default=None, help="Run one named parameter case") - test_parser.add_argument( - "--inspect", action="store_true", default=False, help="Show the resolved test plan" - ) - _ = add_execution_json_output_arg(test_parser) - _ = add_select_args(test_parser) - _ = add_positional_select_arg(test_parser) - _ = add_vars_args(test_parser) - _ = add_dbt_config_args(parser=test_parser) - - check_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.CHECK) - _add_sql_analysis_override(check_parser) - check_parser.add_argument("--json", action="store_true", default=False) - check_parser.add_argument("--target", default=None) - _ = add_execution_json_output_arg(check_parser) - _ = add_select_args(check_parser) - _ = add_positional_select_arg(check_parser) - _ = add_vars_args(check_parser) - _ = add_dbt_config_args(parser=check_parser) - - audit_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.AUDIT) - _add_sql_analysis_override(audit_parser) - audit_parser.add_argument("--defer-to", default=None) - audit_parser.add_argument("--target", default=None) - audit_parser.add_argument("--concurrency", type=int, default=None) - audit_parser.add_argument("--json", action="store_true", default=False) - _ = add_execution_json_output_arg(audit_parser) - _ = add_select_args(audit_parser) - _ = add_positional_select_arg(audit_parser) - _ = add_vars_args(audit_parser) - _ = add_dbt_config_args(parser=audit_parser) - - format_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.FORMAT) - format_parser.add_argument("--json", action="store_true", default=False) - format_parser.add_argument("--check", dest="format_check", action="store_true") - format_parser.add_argument("--diff", dest="format_diff", action="store_true") - format_parser.add_argument( - "--fix", - dest="format_fix", - action="store_true", - help="apply compiler-verified meaning-preserving Rule fixes", - ) - format_parser.add_argument( - "--fixtures-only", - dest="format_fixtures_only", - action="store_true", - help="only remove redundant typed-null SQL test fixture columns", - ) - format_parser.add_argument( - "format_paths", - nargs="*", - default=[], - metavar="PATH", - help="SQL files or folders to format, relative to the current directory", - ) - _ = add_select_args(format_parser) - - -def _add_data_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - load_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.LOAD) - load_parser.add_argument("--reload", action="store_true", default=False) - load_parser.add_argument("--json", action="store_true", default=False) - load_parser.add_argument("--target", default=None) - load_parser.add_argument("--concurrency", type=int, default=None) - _ = add_execution_json_output_arg(load_parser) - _ = add_select_args(load_parser) - _ = add_cursor_override_args(load_parser) - _ = add_vars_args(load_parser) - - seed_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.SEED) - seed_parser.add_argument("--json", action="store_true", default=False) - seed_parser.add_argument("--target", default=None) - seed_parser.add_argument("--concurrency", type=int, default=None) - _ = add_execution_json_output_arg(seed_parser) - _ = add_select_args(seed_parser) - _ = add_positional_select_arg(seed_parser) - _ = add_vars_args(seed_parser) - - clone_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.CLONE) - _add_sql_analysis_override(clone_parser) - clone_parser.add_argument("--from", dest="from_target", required=True) - clone_parser.add_argument("--to", dest="to_target", default=None) - clone_parser.add_argument("--hard-copy", action="store_true", default=False) - clone_parser.add_argument("--verbose", "-v", action="store_true", default=False) - _ = add_select_args(clone_parser) - _ = add_vars_args(clone_parser) - _ = add_dbt_config_args(parser=clone_parser) - _ = add_execution_json_output_arg(clone_parser) - clone_parser.add_argument("--event-output", type=Path, default=None, help=argparse.SUPPRESS) - - diff_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.DIFF) - diff_parser.add_argument("target_range", nargs="?", metavar="FROM[:TO]") - _add_sql_analysis_override(diff_parser) - diff_parser.add_argument("--full", action="store_true", default=False) - diff_parser.add_argument("--schema-only", action="store_true", default=False) - diff_parser.add_argument("--bounded", default=None) - diff_parser.add_argument("--verbose", "-v", action="store_true", default=False) - diff_parser.add_argument("--max-column-examples", type=int, default=None) - diff_parser.add_argument("--max-row-only-examples", type=int, default=None) - diff_parser.add_argument("--sample-rows", type=int, default=None) - diff_parser.add_argument("--sample-seed", type=int, default=None) - diff_parser.add_argument("--exhaustive", action="store_true", default=False) - diff_parser.add_argument("--max-models", type=int, default=None) - diff_parser.add_argument("--max-columns", type=int, default=None) - diff_parser.add_argument( - "--target", - default=None, - help="target connection for raw-query diff (defaults to the active target)", - ) - diff_parser.add_argument("--left-query", default=None, help="literal left-side SQL query") - diff_parser.add_argument( - "--left-query-file", type=Path, default=None, help="path to left-side SQL query" - ) - diff_parser.add_argument("--right-query", default=None, help="literal right-side SQL query") - diff_parser.add_argument( - "--right-query-file", type=Path, default=None, help="path to right-side SQL query" - ) - diff_parser.add_argument("--left-label", default=None, help="display label for the left query") - diff_parser.add_argument( - "--right-label", default=None, help="display label for the right query" - ) - diff_parser.add_argument( - "--key", - nargs="+", - action="extend", - default=[], - help="row identity columns; several values or repeated flags form a composite key", - ) - diff_parser.add_argument( - "--unkeyed", - action="store_true", - default=False, - help="compare exact full-row multiplicities without a key", - ) - diff_parser.add_argument( - "--exclude-column", - nargs="+", - action="extend", - default=[], - help="columns to omit from row comparison; takes several values or repeated flags", - ) - diff_parser.add_argument( - "--tolerance", - action="append", - default=[], - help="numeric tolerance as COLUMN:absolute=VALUE or COLUMN:relative=VALUE", - ) - diff_parser.add_argument( - "--max-value-length", - type=int, - default=None, - help="maximum rendered characters per example value", - ) - diff_parser.add_argument( - "--no-example-values", - action="store_true", - default=False, - help="show example keys and counts without values", - ) - diff_parser.add_argument( - "--full-example-values", - action="store_true", - default=False, - help="explicitly render complete example values", - ) - diff_parser.add_argument( - "--json", - action="store_true", - default=False, - help="write structured diff output to stdout", - ) - _ = add_execution_json_output_arg(diff_parser) - _ = add_select_args(diff_parser) - _ = add_vars_args(diff_parser) - - -def _add_inspection_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - scope_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.SCOPE) - scope_parser.add_argument("scope_target", nargs="?", metavar="TARGET") - scope_parser.add_argument("--at", dest="scope_at", default=None, metavar="PATH") - scope_parser.add_argument("--as-path", dest="scope_as_path", default=None, metavar="PATH") - browse_group: argparse._MutuallyExclusiveGroup = scope_parser.add_mutually_exclusive_group() - browse_group.add_argument("--browse", dest="scope_browse", default=None, metavar="PATH") - browse_group.add_argument("--list", dest="scope_list", default=None, metavar="PATH") - scope_parser.add_argument("--defined-under", dest="scope_defined_under", default=None) - scope_parser.add_argument( - "--kind", - dest="scope_kind", - action="append", - choices=tuple(kind.value for kind in DeclarationKind), - default=[], - ) - scope_parser.add_argument("--match", dest="scope_match", default=None, metavar="GLOB") - scope_parser.add_argument("--used-only", dest="scope_used_only", action="store_true") - scope_parser.add_argument("--include-nearby", dest="scope_include_nearby", action="store_true") - scope_parser.add_argument("--nearby-depth", dest="scope_nearby_depth", type=int, default=1) - scope_parser.add_argument( - "--dependency-depth", dest="scope_dependency_depth", type=int, default=0 - ) - scope_parser.add_argument("--explain", dest="scope_explain", default=None) - scope_parser.add_argument( - "--globals", dest="scope_globals", choices=("summary", "used", "all"), default="summary" - ) - scope_parser.add_argument("--page-size", dest="scope_page_size", type=int, default=100) - scope_parser.add_argument("--after", dest="scope_after", default=None, metavar="CURSOR") - scope_parser.add_argument( - "--paths", dest="scope_paths", choices=("relative", "compact", "none"), default="relative" - ) - scope_parser.add_argument("--json", action="store_true", default=False) - scope_parser.add_argument("--no-cache", action="store_true", default=False) - scope_parser.add_argument("--verbose", "-v", action="store_true", default=False) - - debug_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.DEBUG) - debug_parser.add_argument("--json", action="store_true", default=False) - debug_parser.add_argument("--target", default=None) - debug_parser.add_argument("--no-connection", action="store_true", default=False) - - cost_parser: argparse.ArgumentParser = subparsers.add_parser( - CliCommand.COST, - help="show persisted Snowflake busy-compute estimates", - ) - cost_parser.add_argument( - "cost_selector", - nargs="?", - default="latest", - metavar="latest|history|run_id", - ) - cost_parser.add_argument( - "--limit", dest="cost_limit", type=int, default=None, help="limit returned rows" - ) - cost_parser.add_argument( - "--no-limit", - dest="cost_no_limit", - action="store_true", - default=False, - help="return all matching rows", - ) - cost_parser.add_argument( - "--sort", dest="cost_sort", default=None, help="metric used to sort results" - ) - cost_parser.add_argument( - "--order", - dest="cost_order", - choices=("asc", "desc"), - default=None, - help="sort direction", - ) - cost_parser.add_argument( - "--since", - dest="cost_since", - default=None, - help="inclusive ISO date/datetime or relative duration such as 7d", - ) - cost_parser.add_argument( - "--until", - dest="cost_until", - default=None, - help="inclusive ISO date or timezone-aware datetime", - ) - cost_parser.add_argument( - "--json", action="store_true", default=False, help="write stable JSON to stdout" - ) - _ = add_execution_json_output_arg(cost_parser) - - query_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.QUERY) - query_parser.add_argument("query_sql", nargs="?", metavar="sql") - query_parser.add_argument("--file", dest="query_file", default=None) - query_parser.add_argument("--target", default=None) - query_format_group: argparse._MutuallyExclusiveGroup = ( - query_parser.add_mutually_exclusive_group() - ) - query_format_group.add_argument( - "--format", - dest="query_format", - choices=("long", "table", "json", "csv"), - default="long", - ) - _add_json_format_alias(group=query_format_group, dest="query_format") - query_parser.add_argument("--limit", dest="query_limit", type=int, default=20) - query_parser.add_argument( - "--no-limit", dest="query_no_limit", action="store_true", default=False - ) - - lineage_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.LINEAGE) - lineage_parser.add_argument( - "lineage_targets", - nargs="*", - default=[], - metavar="target", - help="models, sources or seeds (optionally kind-prefixed, e.g. model:orders), " - "or one model.column", - ) - _add_sql_analysis_override(lineage_parser) - lineage_format_group: argparse._MutuallyExclusiveGroup = ( - lineage_parser.add_mutually_exclusive_group() - ) - lineage_format_group.add_argument( - "--format", - dest="lineage_format", - choices=("tree", "json", "list"), - default="tree", - ) - _add_json_format_alias(group=lineage_format_group, dest="lineage_format") - lineage_parser.add_argument( - "--include-uses", - dest="lineage_include_uses", - action="store_true", - default=False, - help="include direct filter, join, grouping, window, and ordering column uses", - ) - lineage_parser.add_argument( - "--direction", - dest="lineage_direction", - choices=("upstream", "downstream", "both"), - default=None, - ) - lineage_parser.add_argument("--depth", dest="lineage_depth", default="all") - lineage_parser.add_argument( - "--mode", - dest="lineage_mode", - choices=COLUMN_LINEAGE_MODE_VALUES, - default=ColumnLineageMode.RICH.value, - help="Column lineage mode: rich (default) or fast", - ) - _ = add_select_args(lineage_parser) - _ = add_vars_args(lineage_parser) - - -def _add_json_format_alias(*, group: argparse._MutuallyExclusiveGroup, dest: str) -> None: - group.add_argument( - "--json", - dest=dest, - action="store_const", - const=JSON_OUTPUT_FORMAT, - help="shorthand for --format json", - ) - - -def _add_maintenance_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - subparsers.add_parser(CliCommand.CLEAN) - janitor_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.JANITOR) - janitor_parser.add_argument("--auto-approve", action="store_true", default=False) - janitor_parser.add_argument("--retention-days", type=int, default=None) - janitor_parser.add_argument("--direct-state-history-versions", type=int, default=None) - janitor_parser.add_argument( - "--drop-old-name-view", - action="append", - default=[], - metavar="NAME", - dest="drop_old_name_views", - ) - janitor_parser.add_argument("--target", default=None) - janitor_parser.add_argument( - "--as", - dest="as_target", - default=None, - help=( - "Preview the janitor plan as another configured target through the active " - "target's connection; inspection only" - ), - ) - - -def _add_workspace_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - subparsers.add_parser(CliCommand.INIT) - playground_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.PLAYGROUND) - playground_parser.add_argument( - "playground_path", - nargs="?", - default="sqlbuild-playground", - metavar="path", - ) - playground_parser.add_argument( - "--template", - dest="playground_template", - choices=PLAYGROUND_TEMPLATE_VALUES, - default="waffle_shop", - ) - - scenario_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.SCENARIO) - scenario_subparsers: argparse._SubParsersAction[argparse.ArgumentParser] - scenario_subparsers = scenario_parser.add_subparsers(dest="scenario_command") - scenario_test_parser: argparse.ArgumentParser = scenario_subparsers.add_parser("test") - scenario_test_parser.add_argument("--scenario-namespace", metavar="NAMESPACE") - scenario_test_parser.add_argument("scenario_selector", nargs="*", metavar="scenario") - scenario_test_parser.add_argument("--retain", dest="scenario_retain", action="store_true") - scenario_test_parser.add_argument("--local", dest="scenario_local", action="store_true") - scenario_test_parser.add_argument("--strict", dest="scenario_strict", action="store_true") - scenario_test_parser.add_argument("--concurrency", type=int, default=None) - scenario_test_parser.add_argument("--json", action="store_true", default=False) - _ = add_execution_json_output_arg(scenario_test_parser) - _ = add_select_args(scenario_test_parser) - scenario_snapshot_group: argparse._MutuallyExclusiveGroup = ( - scenario_test_parser.add_mutually_exclusive_group() - ) - scenario_snapshot_group.add_argument( - "--sync-snapshots", dest="scenario_sync_snapshots", action="store_true" - ) - scenario_snapshot_group.add_argument("--refresh", dest="scenario_refresh", action="store_true") - _ = add_scenario_snapshot_safety_args(scenario_test_parser) - scenario_capture_parser: argparse.ArgumentParser = scenario_subparsers.add_parser("capture") - scenario_capture_parser.add_argument("--scenario-namespace", metavar="NAMESPACE") - scenario_capture_parser.add_argument("scenario_selector", nargs="*", metavar="scenario") - scenario_capture_parser.add_argument("--retain", dest="scenario_retain", action="store_true") - _ = add_select_args(scenario_capture_parser) - _ = add_scenario_snapshot_safety_args(scenario_capture_parser) - - -def _add_dbt_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - dbt_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.DBT) - dbt_subparsers: argparse._SubParsersAction[argparse.ArgumentParser] - dbt_subparsers = dbt_parser.add_subparsers(dest="dbt_command") - dbt_subparsers.add_parser("plan", add_help=False) - dbt_subparsers.add_parser("run", add_help=False) - dbt_subparsers.add_parser("build", add_help=False) - dbt_subparsers.add_parser("debug") - dbt_init_parser: argparse.ArgumentParser = dbt_subparsers.add_parser("init") - dbt_init_parser.add_argument("--project-dir", dest="dbt_project_dir", default=None) - dbt_init_parser.add_argument("--profiles-dir", dest="dbt_profiles_dir", default=None) - dbt_init_parser.add_argument("--profile", dest="dbt_profile", default=None) - dbt_init_parser.add_argument("--target", dest="dbt_target", default=None) - dbt_init_parser.add_argument("--sqb-output-dir", dest="sqb_output_dir", default=None) - dbt_init_parser.add_argument("--dry-run", dest="dry_run", action="store_true", default=False) - dbt_init_parser.add_argument( - "--overwrite", dest="overwrite", action="store_true", default=False - ) - dbt_init_parser.add_argument( - "--skip-dbt-debug", dest="skip_dbt_debug", action="store_true", default=False - ) - - -def _add_skills_parsers( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - skills_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.SKILLS) - skills_parser.add_argument("--global", dest="skills_global", action="store_true") - skills_parser.add_argument( - "--target", - dest="skills_target", - action="append", - choices=("opencode", "claude", "agents"), - default=[], - ) - skills_parser.add_argument("--force", dest="skills_force", action="store_true") - - -def _add_rules_parser( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], -) -> None: - rules_parser: argparse.ArgumentParser = subparsers.add_parser(CliCommand.RULES) - rules_parser.add_argument("--json", action="store_true", default=False) - rules_subparsers: argparse._SubParsersAction[argparse.ArgumentParser] = ( - rules_parser.add_subparsers(dest="rules_command", required=True) - ) - _ = rules_subparsers.add_parser("list") - show_parser: argparse.ArgumentParser = rules_subparsers.add_parser("show") - show_parser.add_argument("rules_rule_selector") - run_parser: argparse.ArgumentParser = rules_subparsers.add_parser("run") - run_parser.add_argument("rules_rule_selector") - add_select_args(run_parser) - skills_parser: argparse.ArgumentParser = rules_subparsers.add_parser("skills") - skills_parser.add_argument("--check", dest="rules_skills_check", action="store_true") diff --git a/src/sqlbuild/cli/commands/_helpers/entry/parser_arguments.py b/src/sqlbuild/cli/commands/_helpers/entry/parser_arguments.py deleted file mode 100644 index 0ac183d1bd..0000000000 --- a/src/sqlbuild/cli/commands/_helpers/entry/parser_arguments.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Reusable root CLI parser argument construction.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -def parse_cli_vars(value: str) -> dict[str, object]: - """Parse SQLBuild CLI project vars from a JSON object.""" - - try: - parsed: Any = json.loads(value) - except json.JSONDecodeError as error: - raise argparse.ArgumentTypeError(f"--vars must be valid JSON: {error.msg}") from error - if not isinstance(parsed, dict): - raise argparse.ArgumentTypeError("--vars must be a JSON object") - result: dict[str, object] = {} - key: object - item_value: object - for key, item_value in parsed.items(): - if not isinstance(key, str): - raise argparse.ArgumentTypeError("--vars keys must be strings") - result[key] = item_value - return result - - -def add_vars_args(parser: argparse.ArgumentParser) -> None: - """Add SQLBuild project variable override flags to a subparser.""" - - parser.add_argument("--vars", dest="vars", type=parse_cli_vars, default={}) - - -def add_cursor_override_args(parser: argparse.ArgumentParser) -> None: - """Add cursor override flags to a subparser.""" - - parser.add_argument( - "--start-cursor", - default=None, - metavar="VALUE", - help="override the start cursor, typed from the selected models' cursor type", - ) - parser.add_argument( - "--end-cursor", - default=None, - metavar="VALUE", - help="override the end cursor, typed from the selected models' cursor type", - ) - parser.add_argument("--start-cursor-ts", default=None, help="timestamp start cursor override") - parser.add_argument("--end-cursor-ts", default=None, help="timestamp end cursor override") - parser.add_argument("--start-cursor-int", default=None, help="integer start cursor override") - parser.add_argument("--end-cursor-int", default=None, help="integer end cursor override") - - -def add_microbatch_limit_override_arg(parser: argparse.ArgumentParser) -> None: - """Add the intentional per-model microbatch limit override.""" - - parser.add_argument("--max-microbatches", type=_positive_integer, default=None) - - -def _positive_integer(value: str) -> int: - parsed: int = int(value) - if parsed < 1: - raise argparse.ArgumentTypeError("value must be a positive integer") - return parsed - - -def add_execution_args(parser: argparse.ArgumentParser) -> None: - """Add execution control flags shared by build and run commands.""" - - parser.add_argument("--fail-fast", action="store_true", default=False) - parser.add_argument("--full-refresh", action="store_true", default=False) - parser.add_argument("--allow-snapshot-full-refresh", action="store_true", default=False) - parser.add_argument("--allow-table-type-downgrade", action="store_true", default=False) - parser.add_argument("--allow-retention-decrease", action="store_true", default=False) - parser.add_argument("--allow-missing-migration-origin", action="store_true", default=False) - parser.add_argument("--allow-snapshot-schema-change", action="store_true", default=False) - parser.add_argument("--concurrency", type=int, default=None) - parser.add_argument("--verbose", "-v", action="store_true", default=False) - - -def add_execution_json_output_arg(parser: argparse.ArgumentParser) -> None: - """Add structured execution JSON output flags.""" - - parser.add_argument("--json-output", type=Path, default=None) - - -def add_select_args(parser: argparse.ArgumentParser) -> None: - """Add --select and --exclude flags for scope selection.""" - - parser.add_argument("--select", "-s", nargs="+", action="extend", default=[]) - parser.add_argument("--select-file", action="append", default=[]) - parser.add_argument("--exclude", nargs="+", action="extend", default=[]) - - -def add_positional_select_arg(parser: argparse.ArgumentParser) -> None: - """Accept selectors as positional arguments; they are combined with --select.""" - - parser.add_argument( - "positional_select", - nargs="*", - default=[], - metavar="SELECTOR", - help="selectors to include, the same as --select; combined with any --select values", - ) - - -def add_dbt_config_args(*, parser: argparse.ArgumentParser, prefix: str = "dbt") -> None: - """Add dbt configuration flags to a subparser.""" - - flag_prefix: str = f"{prefix}-" if prefix else "" - dest_prefix: str = f"{prefix}_" if prefix else "" - parser.add_argument( - f"--{flag_prefix}project-dir", dest=f"{dest_prefix}project_dir", default=None - ) - parser.add_argument( - f"--{flag_prefix}profiles-dir", dest=f"{dest_prefix}profiles_dir", default=None - ) - parser.add_argument(f"--{flag_prefix}target", dest=f"{dest_prefix}target", default=None) - parser.add_argument( - f"--{flag_prefix}target-path", dest=f"{dest_prefix}target_path", default=None - ) - - -def add_scenario_snapshot_safety_args(parser: argparse.ArgumentParser) -> None: - """Add scenario snapshot capture safety flags to a subparser.""" - - parser.add_argument("--force", dest="scenario_force", action="store_true", default=False) - parser.add_argument( - "--max-snapshot-rows", - dest="scenario_max_snapshot_rows", - type=int, - default=None, - ) - parser.add_argument( - "--max-snapshot-total-rows", - dest="scenario_max_snapshot_total_rows", - type=int, - default=None, - ) - parser.add_argument( - "--max-snapshot-bytes", - dest="scenario_max_snapshot_bytes", - type=int, - default=None, - ) - parser.add_argument( - "--max-snapshot-total-bytes", - dest="scenario_max_snapshot_total_bytes", - type=int, - default=None, - ) diff --git a/src/sqlbuild/cli/commands/_helpers/entry/parsing.py b/src/sqlbuild/cli/commands/_helpers/entry/parsing.py index 56bc7f1221..d5bfce344c 100644 --- a/src/sqlbuild/cli/commands/_helpers/entry/parsing.py +++ b/src/sqlbuild/cli/commands/_helpers/entry/parsing.py @@ -1,62 +1,19 @@ -"""CLI argument parsing with post-parse normalization.""" +"""CLI argument parsing through the native argv parser, with post-parse normalization.""" from __future__ import annotations -import argparse -import os +import shutil +import sys from collections.abc import Sequence from pathlib import Path +import sqlbuild._native as _native from sqlbuild.cli.commands.classes.cli_namespace import CliNamespace -from sqlbuild.cli.commands.constants import ( - DBT_PASSTHROUGH_SEPARATOR, - DBT_VERBOSE_OPTIONS, - DEBUG_OPTION, - EMPTY_ENV_VALUE, - NO_COLOR_OPTION, - POSITIONAL_SELECT_COMMANDS, - SCOPE_DEFAULT_PAGE_SIZE, - SCOPE_GLOBAL_SUMMARY, - SQLBUILD_CONCURRENCY_ENV_VAR, -) -from sqlbuild.cli.commands.types import CliCommand from sqlbuild.cli.entry.models import ( ParsedCliInvocation, SelectorFileSummary, SelectorInputs, ) -from sqlbuild.compiler.scopes.types import DeclarationKind -from sqlbuild.integrations.dbt.types import DbtInteropCommand - -_DBT_PASSTHROUGH_SUBCOMMANDS: frozenset[str] = frozenset( - { - "plan", - "run", - "build", - "debug", - } -) - - -def resolve_env_default_concurrency(explicit_concurrency: int | None) -> int | None: - """Return CLI concurrency, falling back to SQLBUILD_CONCURRENCY when unset.""" - - if explicit_concurrency is not None: - if explicit_concurrency < 1: - raise argparse.ArgumentTypeError("--concurrency must be >= 1") - return explicit_concurrency - raw_value: str | None = os.environ.get(SQLBUILD_CONCURRENCY_ENV_VAR) - if raw_value is None or raw_value == EMPTY_ENV_VALUE: - return None - try: - concurrency: int = int(raw_value) - except ValueError as error: - raise argparse.ArgumentTypeError( - f"{SQLBUILD_CONCURRENCY_ENV_VAR} must be an integer" - ) from error - if concurrency < 1: - raise argparse.ArgumentTypeError(f"{SQLBUILD_CONCURRENCY_ENV_VAR} must be >= 1") - return concurrency def read_selector_file_inputs(paths: list[str]) -> SelectorInputs: @@ -77,149 +34,23 @@ def read_selector_file_inputs(paths: list[str]) -> SelectorInputs: return SelectorInputs(selectors=tuple(selectors), files=tuple(files)) -def parse_cli_invocation( - *, - argv: Sequence[str] | None, - parser: argparse.ArgumentParser, -) -> ParsedCliInvocation: - """Parse CLI arguments into a namespace, resolving passthrough and env defaults.""" +def parse_cli_invocation(*, argv: Sequence[str] | None, use_color: bool) -> ParsedCliInvocation: + """Parse CLI arguments into a namespace; help, version and errors print and exit.""" - try: - args: CliNamespace = CliNamespace() - unknown_args: list[str] - _, unknown_args = parser.parse_known_args(argv, namespace=args) - if args.command == CliCommand.DBT and args.dbt_command in _DBT_PASSTHROUGH_SUBCOMMANDS: - dbt_passthrough_args: list[str] = [] - dbt_passthrough_started: bool = False - dbt_arg: str - for dbt_arg in unknown_args: - if dbt_arg == DBT_PASSTHROUGH_SEPARATOR: - dbt_passthrough_started = True - dbt_passthrough_args.append(dbt_arg) - continue - if dbt_arg == NO_COLOR_OPTION and not dbt_passthrough_started: - args.no_color = True - continue - if dbt_arg == DEBUG_OPTION and not dbt_passthrough_started: - args.debug = True - continue - dbt_passthrough_args.append(dbt_arg) - args.dbt_args = dbt_passthrough_args - else: - if args.command in POSITIONAL_SELECT_COMMANDS: - args.select, unknown_args = _merge_positional_selectors( - positional=args.positional_select, - select=args.select, - unknown_args=unknown_args, - ) - if unknown_args: - parser.error(f"unrecognized arguments: {' '.join(unknown_args)}") - args.verbose = args.verbose or args.debug - if ( - args.debug - and args.command == CliCommand.DBT - and args.dbt_command != DbtInteropCommand.DEBUG - and not any(option in args.dbt_args for option in DBT_VERBOSE_OPTIONS) - ): - args.dbt_args.append("--verbose") - if args.command in { - CliCommand.AUDIT, - CliCommand.BUILD, - CliCommand.LOAD, - CliCommand.SEED, - CliCommand.TEST, - }: - try: - args.concurrency = resolve_env_default_concurrency(args.concurrency) - except argparse.ArgumentTypeError as error: - parser.error(str(error)) - if args.command in {CliCommand.BUILD, CliCommand.LOAD, CliCommand.PLAN}: - _validate_cursor_override_args(args=args, parser=parser) - if args.command == CliCommand.SCOPE: - _validate_scope_args(args=args, parser=parser) - if args.command == CliCommand.TEST: - _validate_test_args(args=args, parser=parser) - if args.command == CliCommand.JANITOR and args.as_target is not None and args.auto_approve: - parser.error( - "janitor --as is inspection only and cannot be combined with --auto-approve" - ) - except SystemExit as error: - exit_code: int = error.code if isinstance(error.code, int) else 1 + args: CliNamespace = CliNamespace() + raw_args: list[str] = list(sys.argv[1:] if argv is None else argv) + rows: list[tuple[str, object]] | None + exit_code: int | None + rows, exit_code = _native.parse_cli_arguments( + raw_args, + list(vars(args).items()), + use_color=use_color, + columns=shutil.get_terminal_size().columns, + ) + if rows is None: return ParsedCliInvocation(args=None, exit_code=exit_code) + name: str + value: object + for name, value in rows: + setattr(args, name, value) return ParsedCliInvocation(args=args, exit_code=None) - - -def _merge_positional_selectors( - *, positional: list[str], select: list[str], unknown_args: list[str] -) -> tuple[list[str], list[str]]: - """Combine positional, trailing and --select selectors; return what is still unrecognised.""" - - if unknown_args and not any(arg.startswith("-") for arg in unknown_args): - return [*positional, *unknown_args, *select], [] - return [*positional, *select], unknown_args - - -def _validate_cursor_override_args(*, args: CliNamespace, parser: argparse.ArgumentParser) -> None: - has_untyped: bool = args.start_cursor is not None or args.end_cursor is not None - has_typed: bool = any( - value is not None - for value in ( - args.start_cursor_ts, - args.end_cursor_ts, - args.start_cursor_int, - args.end_cursor_int, - ) - ) - if has_untyped and has_typed: - parser.error( - "--start-cursor/--end-cursor cannot be combined with the -ts or -int cursor flags" - ) - - -def _validate_scope_args(*, args: CliNamespace, parser: argparse.ArgumentParser) -> None: - has_target: bool = args.scope_target is not None - has_at: bool = args.scope_at is not None - if has_target == has_at: - parser.error("scope requires exactly one of TARGET or --at PATH") - if args.scope_as_path is not None and not has_target: - parser.error("scope --as-path requires TARGET") - folder_mode: bool = args.scope_browse is not None or args.scope_list is not None - if folder_mode and ( - args.scope_as_path is not None - or args.scope_explain is not None - or args.scope_include_nearby - or args.scope_dependency_depth != 0 - ): - parser.error( - "scope --browse/--list cannot be combined with --as-path, --explain, " - "--include-nearby, or --dependency-depth" - ) - if args.scope_browse is not None and ( - args.scope_defined_under is not None - or args.scope_kind - or args.scope_match is not None - or args.scope_used_only - or args.scope_globals != SCOPE_GLOBAL_SUMMARY - or args.scope_page_size != SCOPE_DEFAULT_PAGE_SIZE - ): - parser.error( - "scope --browse is folder-only; use --list for declaration filters and pagination" - ) - if args.scope_page_size < 1: - parser.error("scope --page-size must be positive") - if args.scope_nearby_depth < 0 or args.scope_dependency_depth < 0: - parser.error("scope depth values must be nonnegative") - if args.scope_explain is not None: - prefix: str = args.scope_explain.partition(":")[0] - qualified_name: str = args.scope_explain.partition(":")[2] - try: - DeclarationKind(prefix) - except ValueError: - parser.error("scope --explain requires a declaration-qualified identity") - if not qualified_name: - parser.error("scope --explain requires a declaration-qualified identity") - - -def _validate_test_args(*, args: CliNamespace, parser: argparse.ArgumentParser) -> None: - if args.inspect and (args.json or args.json_output is not None): - parser.error("test --inspect cannot be combined with --json or --json-output") diff --git a/src/sqlbuild/cli/commands/classes/sqlbuild_argument_parser.py b/src/sqlbuild/cli/commands/classes/sqlbuild_argument_parser.py deleted file mode 100644 index 55cecd7bad..0000000000 --- a/src/sqlbuild/cli/commands/classes/sqlbuild_argument_parser.py +++ /dev/null @@ -1,21 +0,0 @@ -"""SQLBuild argument parser behavior.""" - -from __future__ import annotations - -import argparse -import sys -from typing import NoReturn - -from sqlbuild.presentation.classes.cli_style import CliStyle - - -class SqlbuildArgumentParser(argparse.ArgumentParser): - """Argument parser that renders parser failures with a stable code.""" - - use_color: bool = False - - def error(self, message: str) -> NoReturn: - self.print_usage(sys.stderr) - style: CliStyle = CliStyle(use_color=self.use_color) - prefix: str = style.error_strong("error[C900]:") - self.exit(2, f"{self.prog}: {prefix} {message}\n\n") diff --git a/src/sqlbuild/cli/commands/constants.py b/src/sqlbuild/cli/commands/constants.py index 83b25c8b9f..c3fbff3d6b 100644 --- a/src/sqlbuild/cli/commands/constants.py +++ b/src/sqlbuild/cli/commands/constants.py @@ -2,25 +2,20 @@ from types import MappingProxyType -from sqlbuild.cli.commands.types import CliCommand, CompileLineageMode, PlaygroundTemplate -from sqlbuild.compiler.lineage.types import ColumnLineageMode +from sqlbuild.cli.commands.types import CliCommand, PlaygroundTemplate from sqlbuild.spec.contracts.types import WarehouseGroup C0_CONTROL_CODE_LIMIT: int = 32 C1_CONTROL_CODE_START: int = 127 C1_CONTROL_CODE_LIMIT: int = 160 -COMPILE_LINEAGE_MODE_VALUES: tuple[str, ...] = tuple(mode.value for mode in CompileLineageMode) RICH_LINEAGE_STATUS_MODEL_THRESHOLD: int = 100 EMPTY_DAG_PATH: str = "" TARGET_DIRECTORY_NAME: str = "target" SQLBUILD_CONCURRENCY_ENV_VAR: str = "SQLBUILD_CONCURRENCY" -EMPTY_ENV_VALUE: str = "" NO_COLOR_OPTION: str = "--no-color" -DEBUG_OPTION: str = "--debug" DBT_INIT_COMMAND: str = "init" SCENARIO_TEST_COMMAND: str = "test" SCENARIO_CAPTURE_COMMAND: str = "capture" -COLUMN_LINEAGE_MODE_VALUES: tuple[str, ...] = tuple(mode.value for mode in ColumnLineageMode) UNLIMITED_DEPTH_VALUE: str = "all" UPSTREAM_DIRECTION: str = "upstream" DOWNSTREAM_DIRECTION: str = "downstream" @@ -64,7 +59,6 @@ GRAPH_SELECTOR_EXPANSION_MARKER: str = "+" GRAPH_SELECTOR_PATH_MARKER: str = "~" DBT_JSON_OUTPUT_OPTION: str = "--json" -DBT_PASSTHROUGH_SEPARATOR: str = "--" DBT_VERBOSE_OPTIONS: frozenset[str] = frozenset({"--verbose", "-v"}) DBT_CLI_OUTPUT_OPTIONS: frozenset[str] = frozenset({DBT_JSON_OUTPUT_OPTION, *DBT_VERBOSE_OPTIONS}) DBT_NO_CONNECTION_OPTION: str = "--no-connection" @@ -132,18 +126,6 @@ CLI_WAREHOUSE_SOURCE: str = "--warehouse" CONNECTION_WAREHOUSE_SOURCE: str = "connection warehouse" CONNECTION_DEFAULT_WAREHOUSE_SOURCE: str = "connection default" -POSITIONAL_SELECT_COMMANDS: frozenset[str] = frozenset( - { - CliCommand.AUDIT, - CliCommand.BUILD, - CliCommand.CHECK, - CliCommand.COMPILE, - CliCommand.FRESHNESS, - CliCommand.PLAN, - CliCommand.SEED, - CliCommand.TEST, - } -) COMMAND_WAREHOUSE_GROUPS: MappingProxyType[CliCommand, WarehouseGroup | None] = MappingProxyType( { CliCommand.AUDIT: WarehouseGroup.BUILD, diff --git a/src/sqlbuild/cli/commands/main/entrypoint/entry.py b/src/sqlbuild/cli/commands/main/entrypoint/entry.py index f294f909a1..22a1ef8f9c 100644 --- a/src/sqlbuild/cli/commands/main/entrypoint/entry.py +++ b/src/sqlbuild/cli/commands/main/entrypoint/entry.py @@ -2,12 +2,10 @@ from __future__ import annotations -import argparse from collections.abc import Mapping, Sequence from sqlbuild.cli.commands._helpers.entry.errors import cli_error_use_color from sqlbuild.cli.commands._helpers.entry.lazy_handlers import build_lazy_cli_handlers -from sqlbuild.cli.commands._helpers.entry.parser import build_cli_parser from sqlbuild.cli.commands._helpers.entry.parsing import parse_cli_invocation from sqlbuild.cli.commands.classes.cli_namespace import CliNamespace from sqlbuild.cli.entry.models import ( @@ -49,8 +47,7 @@ def _main_with_dependencies( ): with diagnostics_context(sqlbuild_invocation_id=identity.invocation_id): use_color: bool = cli_error_use_color(argv=argv, supports_color=supports_color) - parser: argparse.ArgumentParser = build_cli_parser(use_color=use_color) - invocation: ParsedCliInvocation = parse_cli_invocation(argv=argv, parser=parser) + invocation: ParsedCliInvocation = parse_cli_invocation(argv=argv, use_color=use_color) if invocation.args is None: return invocation.exit_code if invocation.exit_code is not None else 1 args: CliNamespace = invocation.args diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_native_allocator_entry.py b/tests/integration/src/sqlbuild/cli/commands/main/test_native_allocator_entry.py index 3a9f7b5e22..f284fb5cef 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_native_allocator_entry.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_native_allocator_entry.py @@ -67,7 +67,7 @@ def test_given_fresh_process_when_running_cli_entry_then_purge_delay_is_set_befo assert payload == { "loaded_at_import": False, - "loaded_by_version": False, + "loaded_by_version": True, "purge_delay": test_case.expected_purge_delay, "exit_code": 0, } diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_startup_import_footprint.py b/tests/integration/src/sqlbuild/cli/commands/main/test_startup_import_footprint.py index 7500ee8e9d..bd04eddc48 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_startup_import_footprint.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_startup_import_footprint.py @@ -38,7 +38,6 @@ "polyglot_sql", "pydantic", "yaml", - "sqlbuild._native", "sqlbuild.adapter.contract.classes.base_adapter", "sqlbuild.cli.compile.models", "sqlbuild.cli.output.models", diff --git a/tests/integration/src/sqlbuild/cli/entry/_test_types.py b/tests/integration/src/sqlbuild/cli/entry/_test_types.py index a0bad44aca..71438db67b 100644 --- a/tests/integration/src/sqlbuild/cli/entry/_test_types.py +++ b/tests/integration/src/sqlbuild/cli/entry/_test_types.py @@ -54,3 +54,22 @@ class ReuseFileMutationTestCase: prepare: Callable[[Path], Path] expected_misses: int expected_fragment: str + + +@dataclass(frozen=True) +class ParserErrorTestCase: + """One rejected command line and the coded error line `sqb` ends with.""" + + description: str + args: tuple[str, ...] + expected_usage: bytes + expected_error: bytes + + +@dataclass(frozen=True) +class SelectorCompileTestCase: + """One compile command line and the model its JSON output names.""" + + description: str + args: tuple[str, ...] + expected_model: bytes diff --git a/tests/integration/src/sqlbuild/cli/entry/test_argument_parsing.py b/tests/integration/src/sqlbuild/cli/entry/test_argument_parsing.py new file mode 100644 index 0000000000..91549424bc --- /dev/null +++ b/tests/integration/src/sqlbuild/cli/entry/test_argument_parsing.py @@ -0,0 +1,82 @@ +from pathlib import Path + +import pytest + +from tests.integration.src.sqlbuild.cli.entry._test_types import ( + ParserErrorTestCase, + SelectorCompileTestCase, +) +from tests.integration.src.sqlbuild.cli.entry.helpers import ( + PYTHON_CLI, + LauncherRun, + run, + write_project, +) + + +@pytest.mark.parametrize( + "test_case", + [ + ParserErrorTestCase( + description="type converter rejection", + args=("compile", "--vars", "[1]"), + expected_usage=b"usage: sqb compile ", + expected_error=b"sqb compile: error[C900]: argument --vars: --vars must be a JSON object", + ), + ParserErrorTestCase( + description="post-parse concurrency rule", + args=("build", "--concurrency", "0"), + expected_usage=b"usage: sqb [-h]", + expected_error=b"sqb: error[C900]: --concurrency must be >= 1", + ), + ParserErrorTestCase( + description="mutually exclusive options", + args=("lineage", "orders", "--json", "--format", "tree"), + expected_usage=b"usage: sqb lineage ", + expected_error=( + b"sqb lineage: error[C900]: argument --format: not allowed with argument --json" + ), + ), + ], + ids=lambda case: case.description, +) +def test_given_rejected_arguments_when_running_sqb_then_usage_and_coded_error_exit_two( + test_case: ParserErrorTestCase, tmp_path: Path +) -> None: + exit_code, stdout, stderr = run(PYTHON_CLI, args=test_case.args, cwd=tmp_path) + + assert (exit_code, stdout) == (2, b"") + assert stderr.startswith(test_case.expected_usage) + assert stderr.endswith(test_case.expected_error + b"\n\n") + + +@pytest.mark.parametrize( + "test_case", + [ + SelectorCompileTestCase( + description="positional selector", + args=("compile", "orders", "--json"), + expected_model=b"orders", + ), + SelectorCompileTestCase( + description="positional selector after an option", + args=("compile", "--json", "orders"), + expected_model=b"orders", + ), + ], + ids=lambda case: case.description, +) +def test_given_positional_selector_when_compiling_then_the_selected_model_compiles( + test_case: SelectorCompileTestCase, tmp_path: Path +) -> None: + project_dir: Path = write_project(tmp_path / "project") + + completed: LauncherRun = run(PYTHON_CLI, args=test_case.args, cwd=project_dir) + + exit_code, stdout, _ = completed + assert exit_code == 0 + assert test_case.expected_model in stdout + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/_test_types.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/_test_types.py index 45ea1cf8fb..65c5d922dc 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/_test_types.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/_test_types.py @@ -112,3 +112,41 @@ class HiddenOptionTestCase: description: str option: str expected_listed: bool + + +@dataclass(frozen=True) +class ChoiceListTestCase: + """One option whose choices come from a Python enum.""" + + description: str + argv: tuple[str, ...] + option: str + expected_choices: tuple[str, ...] + + +@dataclass(frozen=True) +class VarsRejectionTestCase: + """One invalid --vars value and the error it reports.""" + + description: str + vars_value: str + expected_message: str + + +@dataclass(frozen=True) +class TypedValueTestCase: + """One accepted command line and the typed value an attribute ends with.""" + + description: str + argv: tuple[str, ...] + attribute: str + expected_value: object + + +@dataclass(frozen=True) +class HelpOutputTestCase: + """One help request and how its output starts.""" + + description: str + argv: tuple[str, ...] + expected_usage: str diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/test_parsing.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/test_parsing.py index d5b8cc8fe9..2d8f5984f9 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/test_parsing.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/entry/test_parsing.py @@ -3,23 +3,30 @@ from __future__ import annotations from importlib.metadata import version as installed_version +from pathlib import Path import pytest -from sqlbuild.cli.commands._helpers.entry.parser import build_cli_parser from sqlbuild.cli.commands._helpers.entry.parsing import parse_cli_invocation +from sqlbuild.cli.commands.types import CompileLineageMode, PlaygroundTemplate from sqlbuild.cli.entry.models import ( ParsedCliInvocation, ) +from sqlbuild.compiler.lineage.types import ColumnLineageMode +from sqlbuild.compiler.scopes.types import DeclarationKind from tests.unit.src.sqlbuild.cli.commands._helpers.entry._test_types import ( AuditConcurrencyParsingTestCase, + ChoiceListTestCase, DbtProjectDirParsingTestCase, GlobalFlagPlacementTestCase, + HelpOutputTestCase, OutputFormatAliasTestCase, PositionalSelectTestCase, QueryDiffParsingTestCase, RejectedArgumentsTestCase, + TypedValueTestCase, UntypedCursorParsingTestCase, + VarsRejectionTestCase, VerboseCommandTestCase, VersionFlagTestCase, ) @@ -69,7 +76,7 @@ def test_given_raw_query_diff_arguments_when_parsing_then_inputs_remain_distinct ) -> None: parsed: ParsedCliInvocation = parse_cli_invocation( argv=test_case.argv, - parser=build_cli_parser(), + use_color=False, ) assert parsed.args is not None @@ -98,15 +105,15 @@ def test_given_output_mode_when_parsing_verbose_command_then_normalizes_consiste ) -> None: default: ParsedCliInvocation = parse_cli_invocation( argv=test_case.expected_argv, - parser=build_cli_parser(), + use_color=False, ) verbose: ParsedCliInvocation = parse_cli_invocation( argv=(*test_case.expected_argv, "--verbose"), - parser=build_cli_parser(), + use_color=False, ) debug: ParsedCliInvocation = parse_cli_invocation( argv=("--debug", *test_case.expected_argv), - parser=build_cli_parser(), + use_color=False, ) assert default.args is not None @@ -171,9 +178,7 @@ def test_given_audit_concurrency_sources_when_parsing_then_precedence_and_valida ) -> None: monkeypatch.setenv("SQLBUILD_CONCURRENCY", test_case.environment_value or "") - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.exit_code == test_case.expected_exit_code assert getattr(parsed.args, "concurrency", None) == test_case.expected_concurrency @@ -201,7 +206,7 @@ def test_given_version_flag_when_parsing_then_prints_version_and_exits_zero( ) -> None: invocation: ParsedCliInvocation = parse_cli_invocation( argv=test_case.argv, - parser=build_cli_parser(), + use_color=False, ) assert invocation.args is None @@ -260,9 +265,7 @@ def test_given_version_flag_when_parsing_then_prints_version_and_exits_zero( def test_given_global_flags_anywhere_when_parsing_then_they_apply_to_the_invocation( test_case: GlobalFlagPlacementTestCase, ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is not None assert parsed.args.debug is test_case.expected_debug @@ -293,9 +296,7 @@ def test_given_global_flags_anywhere_when_parsing_then_they_apply_to_the_invocat def test_given_dbt_project_dir_when_parsing_then_it_is_not_the_sqlbuild_project_dir( test_case: DbtProjectDirParsingTestCase, ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is not None assert parsed.args.project_dir is None @@ -353,9 +354,7 @@ def test_given_dbt_project_dir_when_parsing_then_it_is_not_the_sqlbuild_project_ def test_given_positional_selectors_when_parsing_then_they_join_the_selection( test_case: PositionalSelectTestCase, ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is not None assert tuple(parsed.args.select) == test_case.expected_select @@ -394,9 +393,7 @@ def test_given_positional_selectors_when_parsing_then_they_join_the_selection( def test_given_json_alias_when_parsing_then_selects_json_format( test_case: OutputFormatAliasTestCase, ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is not None assert getattr(parsed.args, test_case.format_attribute) == test_case.expected_format @@ -424,9 +421,7 @@ def test_given_json_alias_when_parsing_then_selects_json_format( def test_given_untyped_cursor_flags_when_parsing_then_values_are_kept_raw( test_case: UntypedCursorParsingTestCase, ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is not None assert (parsed.args.start_cursor, parsed.args.end_cursor) == ( @@ -454,6 +449,11 @@ def test_given_untyped_cursor_flags_when_parsing_then_values_are_kept_raw( argv=("lineage", "stg_orders", "--json", "--format", "tree"), expected_message="argument --format: not allowed with argument --json", ), + RejectedArgumentsTestCase( + description="undecodable choice is shown escaped", + argv=("compile", "--lineage-mode", "\udcff"), + expected_message="argument --lineage-mode: invalid choice: '\\udcff'", + ), RejectedArgumentsTestCase( description="untyped and typed cursor flags together", argv=("build", "--start-cursor", "5", "--end-cursor-int", "9"), @@ -468,14 +468,170 @@ def test_given_conflicting_or_unknown_arguments_when_parsing_then_exits_with_usa test_case: RejectedArgumentsTestCase, capsys: pytest.CaptureFixture[str], ) -> None: - parsed: ParsedCliInvocation = parse_cli_invocation( - argv=test_case.argv, parser=build_cli_parser() - ) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) assert parsed.args is None assert parsed.exit_code == 2 assert test_case.expected_message in capsys.readouterr().err +@pytest.mark.parametrize( + "test_case", + [ + TypedValueTestCase( + description="path option", + argv=("build", "--json-output", "out/run.json"), + attribute="json_output", + expected_value=Path("out/run.json"), + ), + TypedValueTestCase( + description="json vars", + argv=("build", "--vars", '{"region": "emea", "limits": [1, 2]}'), + attribute="vars", + expected_value={"region": "emea", "limits": [1, 2]}, + ), + TypedValueTestCase( + description="integer option", + argv=("build", "--concurrency", "3"), + attribute="concurrency", + expected_value=3, + ), + TypedValueTestCase( + description="positive integer option", + argv=("build", "--max-microbatches", "2"), + attribute="max_microbatches", + expected_value=2, + ), + TypedValueTestCase( + description="enum namespace default outside compile", + argv=("build",), + attribute="compile_lineage_mode", + expected_value=CompileLineageMode.FAST, + ), + TypedValueTestCase( + description="undecodable selector bytes survive", + argv=("build", "orders\udcff"), + attribute="select", + expected_value=["orders\udcff"], + ), + TypedValueTestCase( + description="compile lineage mode choice is a string", + argv=("compile", "--lineage-mode", "rich"), + attribute="compile_lineage_mode", + expected_value="rich", + ), + ], + ids=lambda case: case.description, +) +def test_given_typed_options_when_parsing_then_values_have_their_python_types( + test_case: TypedValueTestCase, +) -> None: + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) + + assert parsed.args is not None + value: object = getattr(parsed.args, test_case.attribute) + assert value == test_case.expected_value + assert type(value) is type(test_case.expected_value) + + +@pytest.mark.parametrize( + "test_case", + [ + ChoiceListTestCase( + description="compile lineage mode", + argv=("compile", "--lineage-mode"), + option="--lineage-mode", + expected_choices=tuple(mode.value for mode in CompileLineageMode), + ), + ChoiceListTestCase( + description="column lineage mode", + argv=("lineage", "orders", "--mode"), + option="--mode", + expected_choices=tuple(mode.value for mode in ColumnLineageMode), + ), + ChoiceListTestCase( + description="scope declaration kind", + argv=("scope", "orders", "--kind"), + option="--kind", + expected_choices=tuple(kind.value for kind in DeclarationKind), + ), + ChoiceListTestCase( + description="playground template", + argv=("playground", "--template"), + option="--template", + expected_choices=tuple(template.value for template in PlaygroundTemplate), + ), + ], + ids=lambda case: case.description, +) +def test_given_unknown_choice_when_parsing_then_error_lists_the_python_enum_values( + test_case: ChoiceListTestCase, + capsys: pytest.CaptureFixture[str], +) -> None: + parsed: ParsedCliInvocation = parse_cli_invocation( + argv=(*test_case.argv, "not-a-choice"), use_color=False + ) + + choices: str = ", ".join(repr(choice) for choice in test_case.expected_choices) + assert parsed.exit_code == 2 + assert ( + f"argument {test_case.option}: invalid choice: 'not-a-choice' (choose from {choices})" + in capsys.readouterr().err + ) + + +@pytest.mark.parametrize( + "test_case", + [ + VarsRejectionTestCase( + description="malformed json", + vars_value="{orders", + expected_message="argument --vars: --vars must be valid JSON: Expecting property name", + ), + VarsRejectionTestCase( + description="json array", + vars_value="[1]", + expected_message="argument --vars: --vars must be a JSON object", + ), + ], + ids=lambda case: case.description, +) +def test_given_invalid_vars_when_parsing_then_exits_with_coded_usage_error( + test_case: VarsRejectionTestCase, + capsys: pytest.CaptureFixture[str], +) -> None: + parsed: ParsedCliInvocation = parse_cli_invocation( + argv=("build", "--vars", test_case.vars_value), use_color=False + ) + + error: str = capsys.readouterr().err + assert parsed.exit_code == 2 + assert error.startswith("usage: sqb build") + assert f"sqb build: error[C900]: {test_case.expected_message}" in error + + +@pytest.mark.parametrize( + "test_case", + [ + HelpOutputTestCase(description="root help", argv=("-h",), expected_usage="usage: sqb [-h]"), + HelpOutputTestCase( + description="command help", argv=("build", "--help"), expected_usage="usage: sqb build" + ), + ], + ids=lambda case: case.description, +) +def test_given_help_flag_when_parsing_then_prints_help_and_exits_zero( + test_case: HelpOutputTestCase, + capsys: pytest.CaptureFixture[str], +) -> None: + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) + + output: str = capsys.readouterr().out + assert parsed.args is None + assert parsed.exit_code == 0 + assert output.startswith(test_case.expected_usage) + assert "\noptions:\n" in output + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) diff --git a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_warehouses.py b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_warehouses.py index 8acad574d4..273490db96 100644 --- a/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_warehouses.py +++ b/tests/unit/src/sqlbuild/cli/commands/_helpers/runtime/test_warehouses.py @@ -2,19 +2,18 @@ from __future__ import annotations -import argparse from pathlib import Path import pytest -from sqlbuild.cli.commands._helpers.entry.parser import build_cli_parser +from sqlbuild.cli.commands._helpers.entry.parsing import parse_cli_invocation from sqlbuild.cli.commands._helpers.runtime.connection import resolve_project_connection_config from sqlbuild.cli.commands._helpers.runtime.warehouse_scope import command_warehouse_scope from sqlbuild.cli.commands._helpers.runtime.warehouses import describe_connection_warehouse from sqlbuild.cli.commands.constants import COMMAND_WAREHOUSE_GROUPS from sqlbuild.cli.commands.exceptions import CliUserError from sqlbuild.cli.commands.types import CliCommand -from sqlbuild.cli.entry.models import ResolvedWarehouse +from sqlbuild.cli.entry.models import ParsedCliInvocation, ResolvedWarehouse from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs from sqlbuild.spec.contracts.models import LocalTargetConfig, TargetWarehousesConfig from sqlbuild.spec.contracts.types import WarehouseGroup @@ -312,9 +311,10 @@ def test_given_cli_command_when_checking_warehouse_groups_then_it_is_classified( def test_given_grouped_command_when_parsing_warehouse_flag_then_value_is_kept( test_case: WarehouseFlagParseTestCase, ) -> None: - parsed: argparse.Namespace = build_cli_parser().parse_args(list(test_case.argv)) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) - assert parsed.warehouse == test_case.expected_warehouse + assert parsed.args is not None + assert parsed.args.warehouse == test_case.expected_warehouse @pytest.mark.parametrize( @@ -333,9 +333,9 @@ def test_given_command_without_group_when_parsing_warehouse_flag_then_it_is_reje test_case: WarehouseFlagRejectedTestCase, capsys: pytest.CaptureFixture[str], ) -> None: - with pytest.raises(SystemExit): - build_cli_parser().parse_args(list(test_case.argv)) + parsed: ParsedCliInvocation = parse_cli_invocation(argv=test_case.argv, use_color=False) + assert parsed.exit_code == 2 assert test_case.expected_error_fragment in capsys.readouterr().err